196 lines
33 KiB
Markdown
196 lines
33 KiB
Markdown
## Context
|
||
|
||
Хаб (`main` 3931a88) отдаёт по `GET /` и `GET /links` страницу доски связей `src/jira_hub/web/links.html` (~1200 строк,
|
||
из них ~900 — JS без зависимостей). Разделы скрипта: справочники и константы (`EPICS`, `KIND_LABELS`, `KIND`, `PRIO`,
|
||
`LINK_TYPES`, геометрия), состояние и хранилище (`MODEL`, фильтры `S`, выборка `VIS`, раскладка `rel/pos`, `localStorage`
|
||
`graph-<имя>…` с `migrateStore`), разбор (`fromJiraCsv`, `fromJson`, `fromHub`), выборка и анализ (`computeVisible`,
|
||
`criticalPath`, `findProblems`), раскладка и отрисовка (`render`, `relayout`, `renderLane`, `renderCard`, `startDrag`,
|
||
`alignEdges`, `drawEdges`, `orthoPath`, колонки дат), панель (`makeMulti`, легенда, проблемы, таблица), хаб
|
||
(`hubFetch`, `loadHub`, `refreshFromJira`, `watchBatch`, `resumeWatch`), экспорт/импорт JSON (`arm-ka-links/1`).
|
||
Финальное ревью `viewer-live` отложило M-1…M-7 (jira-hub#20): гонки поздних ответов и опроса, бесконечный опрос,
|
||
перезаписываемые сообщения, форма входа, непокрытая ветка `mirror.board(own=False)`.
|
||
|
||
API хаба для интерфейсов уже есть и не меняется: `GET /board` (сырой снимок зеркала), `GET /issues` (фильтры `sprint`,
|
||
`assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`; `limit`/`offset`, без общего
|
||
количества), `GET /issues/{key}` (поля, `links_out`, `links_in`, `note`, 10 последних `commands`), `PUT /issues/{key}/note`
|
||
(поля `fact_status`, `work_kind`, `done_text`, `rest_text`, `owner_note`; 422 на неизвестные), `GET /settings`
|
||
(без токенов; `kind_labels`, `link_types`, `status_labels`), `GET /agent`, `POST /refresh`, `GET /batches/{id}`.
|
||
HTTP — `http.server`, маршруты `ROUTES` с областью токена; не-JSON ответ — `Raw(content_type, bytes)`.
|
||
|
||
На Mac mini стоят Node 24 и npm 11; хаб работает под launchd (`uv run --project <основной checkout> jira-hub serve`).
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** веб-приложение хаба, в которое доска перенесена без потери поведения и раскладок; страница задач зеркала
|
||
с карточкой и правкой заметок; закрыть #20; код фронта, который владелец может читать, а агент — развивать (`viewer-edit`).
|
||
|
||
**Non-Goals:** запись в Jira из интерфейса, страницы очереди/агента/настроек, изменения API, изменения логики доски
|
||
(кроме «Открыть карточку»), доступ из сети.
|
||
|
||
## Decisions
|
||
|
||
1. **Отдельный npm-проект `web/` в корне репозитория.** `web/package.json`, `web/vite.config.ts`, `web/tsconfig.json`,
|
||
исходники `web/src/`, тесты рядом с кодом (`*.test.ts(x)`), фикстуры `web/src/**/__fixtures__/`. Python-пакет не
|
||
меняется. Менеджер пакетов — npm, `package-lock.json` в git, установка `npm ci`.
|
||
2. **Стек.** React 19, TypeScript (strict), Vite, React Router (`createBrowserRouter`, `basename: "/app"`),
|
||
TanStack Query (загрузка, кэш, опрос), Vitest + Testing Library (jsdom), ESLint (typescript-eslint, react-hooks),
|
||
Playwright для e2e. Стили — обычный CSS: переменные и классы `links.html` переносятся в `web/src/styles/`, у страниц —
|
||
свои файлы CSS. UI-кита и библиотеки графов нет. Версии в `package.json` — точные, без `^`.
|
||
3. **Приложение под `/app/`.** Пути API (`/issues`, `/board`, …) заняты, от них зависят агент и MCP; префикс разводит
|
||
адреса приложения и API без изменения API. Vite: `base: "/app/"`, `build.outDir: "../src/jira_hub/web/app"`,
|
||
`emptyOutDir: true`. Маршруты приложения: `/app/board`, `/app/issues`, `/app/issues/:key`; `/app` и `/app/` →
|
||
`/app/board`; неизвестный путь внутри приложения — страница «не найдено» со ссылками на разделы.
|
||
4. **Раздача в хабе** (`api.py`, без области токена — в сборке нет данных):
|
||
- `GET /app/assets/<путь>` — файл из `src/jira_hub/web/app/assets/`; путь проверяется: после `resolve()` он SHALL
|
||
лежать внутри каталога `assets`, иначе 404; `Content-Type` по расширению (`.js` → `text/javascript; charset=utf-8`,
|
||
`.css` → `text/css; charset=utf-8`, `.svg` → `image/svg+xml`, `.png`, `.ico`, `.woff2`, `.map` → `application/json`;
|
||
прочее — `application/octet-stream`); `Cache-Control: public, max-age=31536000, immutable` (имена файлов содержат хэш);
|
||
- прочие `GET /app`, `GET /app/…` — `index.html` сборки, `text/html; charset=utf-8`, `Cache-Control: no-cache`;
|
||
- нет `index.html` (сборка не выполнена) — 503 `text/html` с текстом «Веб-приложение не собрано: выполните
|
||
`make web` в каталоге jira-hub и перезапустите хаб» — на любой `/app…`;
|
||
- `GET /` и `GET /links` — 302 `Location: /app/board`;
|
||
- `PAGE` и `links.html` удаляются. Файлы читаются с диска на каждый запрос (как сейчас `links.html`): новая сборка
|
||
видна без перезапуска.
|
||
5. **Сборка не в git, выкатка с `make web`.** `src/jira_hub/web/app/` — в `.gitignore`. Новый `Makefile` в корне:
|
||
`make web` = `cd web && npm ci && npm run build`; `make check` = `uv run pytest` + `cd web && npm run lint && npm test`.
|
||
Выкатка: `git pull` → `make web` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`. Pytest не зависит от
|
||
сборки: тесты раздачи подкладывают свой каталог сборки (параметр `Hub`/путь, подменяемый в тесте).
|
||
6. **Разработка.** `npm run dev` — Vite на `127.0.0.1:5173` с `base: "/app/"` и прокси всех путей API на хаб
|
||
(`JIRA_HUB_URL`, по умолчанию `http://127.0.0.1:8791` — тестовый хаб); прокси перечисляет префиксы API явно
|
||
(`/board`, `/issues`, `/settings`, `/agent`, `/refresh`, `/batches`, `/commands`, `/sync-runs`).
|
||
7. **Один клиент API `web/src/api/client.ts`.** `fetch` того же хоста с `Authorization: Bearer <токен>`,
|
||
`Accept: application/json`, `cache: "no-store"`. Результат — данные или типизированная ошибка:
|
||
`AuthError` (401), `HubDownError` (сеть или 5xx/502/503), `ApiError {status, error, detail}` (прочие 4xx).
|
||
Типы ответов — `web/src/api/types.ts` по спеке hub-api. Запросы — хуки TanStack Query в `web/src/api/queries.ts`
|
||
(`useBoard`, `useSettings`, `useIssues`, `useIssue`, `useAgent`, `useBatch`) и мутации (`useRefresh`, `useSaveNote`).
|
||
8. **Вход.** Токен — `localStorage["jira-hub-token"]` (тот же ключ, что у `links.html`) и копия в памяти на случай
|
||
закрытого хранилища. Нет токена или любой запрос ответил `AuthError` → экран входа поверх приложения: панели и
|
||
страницы не показываются, запросы не идут (`enabled: false`), поле токена не трогается фоновыми событиями (M-6).
|
||
После ввода — проверка `GET /settings`; при успехе токен сохраняется, кэш запросов сбрасывается. «Сменить токен» в
|
||
оболочке очищает токен и показывает экран входа. Загрузки файла CSV/JSON с экрана входа, как у `links.html`, нет:
|
||
разбору нужны словари из `/settings`, а приложение отдаёт сам хаб, так что без хаба его не открыть; файл загружается
|
||
на доске после входа.
|
||
9. **Оболочка.** Верхняя панель: название «Хаб Jira», ссылки «Доска», «Задачи», «Сменить токен». Граница ошибок
|
||
(`ErrorBoundary`) на уровне страницы: неожиданное исключение отрисовки → сообщение и кнопка «Перезагрузить», панель
|
||
остаётся.
|
||
10. **Доска: модули без React** (`web/src/board/`):
|
||
- `constants.ts` — `EPICS` (запасной словарь имён эпиков для CSV), `KIND`, `PRIO`, `JIRA_URL`, `RELATED`, служебные
|
||
дорожки, геометрия; `KIND_LABELS` и `LINK_TYPES` в коде не задаются — приходят параметром `dicts` из `/settings`
|
||
(`kind_labels`, `link_types`; у `link_types` — обратный словарь «имя Jira → логический тип»);
|
||
- `model/` — `fromHub(snap, dicts)`, `fromCsv(text, fileName, dicts)`, `fromJson(text, fileName)` → `BoardModel`;
|
||
`util.ts` — `sortableDate`, `shortName`, `plural`, `fmtDate`, `notStarted`, `unassigned`;
|
||
- `analysis/` — `computeVisible(model, filters)`, `criticalPath`, `findProblems`, `emptyReason`;
|
||
- `layout/` — раскладка дорожек и карточек (`relayout`, `autoHeight` через переданную функцию измерения,
|
||
`clampToCol`), колонки дат, `alignEdges`, `orthoPath`, `edgePath`;
|
||
- `storage.ts` — ключи `graph-<имя>…`, `migrateStore`, чтение/запись раскладки, дорожек, фильтров, темы; все
|
||
обращения к `localStorage` в `try/catch`, как сейчас.
|
||
Алгоритмы переносятся построчно; переименование и упрощение — только без изменения результата на golden-фикстурах.
|
||
11. **Доска: состояние и компоненты.** Модель и источник — в `useBoardSource` (раздел 12); фильтры, раскладка и
|
||
переключатели — `useReducer` в `BoardPage` с сохранением в `storage.ts`. Компоненты: `Toolbar` (источник, «Перечитать»,
|
||
«Обновить из Jira», «файл…», статистика, «Проблемы», меню «⋯» без «Сменить токен»), `FilterBar` + `MultiSelect`,
|
||
`Legend`, `Board` (контейнер с двумя SVG: `Edges`, `Guides`), `Lane`, `Card`, `DatesBar`, `ProblemsDrawer`,
|
||
`LinkTable`, `ExportBox`, `StatusLine`. Перетаскивание и растяжка — pointer-события на `Card`, во время движения
|
||
положение пишется в состояние раскладки, сохранение — по `pointerup` (как сейчас). Размер карточки «по содержимому»
|
||
измеряется через `ref` на DOM карточки.
|
||
12. **Доска: источник данных и обновление (закрывает M-1…M-5).** `useBoardSource` хранит явный источник:
|
||
`{kind: "hub"}` или `{kind: "file", model, fileName}`. Данные хаба (`useBoard`) показываются только при источнике
|
||
`hub`; загрузка CSV/JSON-снимка переключает источник на `file`, поздний ответ хаба его не затирает (M-1); «Перечитать»
|
||
возвращает источник `hub` и перезапрашивает `/board`. Импорт JSON в режиме хаба применяет раскладку, дорожки и
|
||
фильтры и перечитывает хаб (как сейчас). «Обновить из Jira»: `GET /agent` (офлайн → предупреждение, пакет всё равно
|
||
ставится) → мутация `POST /refresh {full: true, author: "viewer"}` → `useBatch(id)` с `refetchInterval` 3 с до
|
||
терминального статуса пакета → `invalidate(/board)`. Кнопка выключена, пока мутация в пути или есть активный пакет
|
||
(M-2); опрос пакета — один запрос по ключу `["batch", id]`, второго опроса не бывает. Незавершённый пакет `viewer`
|
||
ищется (`GET /batches`) при каждой успешной загрузке `/board`, а не только при первой (M-3). Опрос останавливается
|
||
на 404 сразу и после 5 ошибок `HubDownError` подряд — с сообщением в строке состояния и снова включённой кнопкой (M-4).
|
||
`StatusLine` показывает одно состояние из перечня: «загрузка», «данные хаба на <время>», «из файла <имя>»,
|
||
«пакет #N выполняется», «пакет #N выполнен», «пакет #N: ошибка <текст>», «хаб не отвечает — показаны прежние данные»,
|
||
«агент офлайн — пакет поставлен»; ошибки разбора файла — отдельным текстом «файл не разобран: <причина>», не «хаб не
|
||
отвечает» (M-5).
|
||
13. **Доска: «Открыть карточку».** В заголовке карточки (не у заглушек) рядом с ключом — значок-ссылка «ⓘ» на
|
||
`/app/issues/{key}` (переход внутри приложения). Ключ по-прежнему открывает задачу в Jira, перетаскивание и выделение
|
||
текста не меняются.
|
||
14. **Задачи: список `/app/issues`.** Таблица: ключ, тема, тип, статус Jira, исполнитель, спринт, срок (`planned_end`,
|
||
иначе `duedate`), эпик (имя из снимка `/board`, иначе ключ), вид работы, статус по факту, «сосед». Фильтры — параметры
|
||
`GET /issues`, состояние фильтров — в query-строке адреса (`useSearchParams`); поиск `q` применяется с задержкой 300 мс.
|
||
Варианты выпадающих списков (спринты, исполнители, статусы Jira, эпики, метки, виды работы, статусы по факту) —
|
||
из `useBoard()`, без новых маршрутов. Порции по 50 (`useInfiniteQuery`, `offset`), кнопка «Показать ещё» видна,
|
||
пока последняя порция полная (50 строк). Ключ — ссылка на карточку.
|
||
15. **Задачи: карточка `/app/issues/:key`.** Поля задачи, «открыть в Jira», описание — текст с `white-space: pre-wrap`
|
||
без разбора разметки Jira; связи «исходящие»/«входящие» с подписью типа (логический тип из `link_types`, иначе имя
|
||
Jira), ключ — ссылка на карточку; 10 последних команд: id, пакет, тип, статус, время, ошибка. 404 → «задачи нет в
|
||
зеркале» со ссылкой на список.
|
||
16. **Задачи: заметки.** Форма в карточке: «вид работы» — выпадающий список из значений `kind_labels` (плюс текущее
|
||
значение, если его там нет, и «—» = пусто); «статус по факту» — поле с подсказками (`datalist`) из ключей
|
||
`status_labels`; «сделано», «осталось», «заметка владельца» — многострочные поля. «Сохранить» активна при изменениях
|
||
и отправляет только изменённые поля (`PUT /issues/{key}/note`, `updated_by: "web"`); пустое поле отправляется как
|
||
`null`. Успех → обновлённая заметка в кэше карточки и списка, «сохранено <время>, <кто>». 422 → текст `detail` у
|
||
формы, введённое не теряется. Уход со страницы с несохранёнными правками → подтверждение (`useBlocker` и
|
||
`beforeunload`).
|
||
17. **Golden-эталоны доски из старой страницы.** Скрипт `web/scripts/golden-from-links.mjs` (разовый, в git): читает
|
||
`links.html` из git-ревизии 3931a88, исполняет его `<script>` в `node:vm` с минимальной заглушкой DOM, вызывает
|
||
`fromHub`/`fromJiraCsv`/`fromJson`, `computeVisible` при наборах фильтров, `criticalPath`, `findProblems`, `orthoPath`
|
||
и пишет `<name>.expected.json` рядом с фикстурами. Фикстуры — синтетические (`<name>.board.json`, `<name>.csv`,
|
||
`<name>.layout.json`): эпики, соседи, заглушки «нет в зеркале», все три типа связей и неизвестный тип, несколько
|
||
спринтов, задачи без исполнителя и срока, передачи между людьми, цепочка для критического пути. Тесты Vitest
|
||
сравнивают выход TS-модулей с эталоном побайтно после `JSON.stringify` с сортировкой ключей.
|
||
18. **Проверки.** Vitest: golden-модули доски, `client.ts` (401/сеть/4xx), `storage.ts` (ключи, миграция, закрытое
|
||
хранилище), `useBoardSource` (M-1…M-5 сценариями с подменённым `fetch` и таймерами), «Задачи» (фильтры в адресе,
|
||
«Показать ещё», заметка шлёт только изменённое, 422, подтверждение ухода). Pytest: раздача `/app…` (типы, кэш,
|
||
`index.html` на произвольный путь, 503 без сборки, обход каталога → 404, редиректы `/`/`/links`, без токена), тест
|
||
`mirror.board` внутри открытой транзакции (M-7). Playwright e2e на тестовом хабе `:8791`
|
||
(`JIRA_HUB_HOME` во временном каталоге, зеркало заполнено `POST /agent/state` из golden-фикстуры): вход и неверный
|
||
токен, доска (перетаскивание, растяжка, стрелки, «по датам», «Обновить из Jira» до конца пакета с учебным агентом,
|
||
«Открыть карточку»), задачи (фильтр → карточка → заметка). До удаления `links.html` — скриншоты старой и новой доски
|
||
на одной фикстуре и одной раскладке, расхождения разбираются вручную. `tsc --noEmit` и ESLint — в `npm run lint`.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Переписывание ~900 строк может изменить поведение доски незаметно] → golden-эталоны старой страницы на модулях
|
||
логики, e2e и сверка скриншотов на отрисовке; перенос построчный, улучшения — только без изменения результата.
|
||
- [Перетаскивание через состояние React может тормозить на ~200 карточках] → перерисовывается только двигаемая
|
||
карточка и стрелки (`memo` на `Card`, раскладка — в отдельном контексте); если e2e покажет задержку — положение во время
|
||
движения через `ref`/`transform`, в состояние — по `pointerup`.
|
||
- [Сборка не в git: после `git pull` без `make web` хаб отдаёт старую сборку или 503] → 503 с явной подсказкой; шаг
|
||
выкатки в README и Migration Plan.
|
||
- [Зависимости npm — поверхность цепочки поставок] → точные версии, `package-lock.json`, `npm ci`; зависимостей
|
||
времени выполнения четыре (react, react-dom, react-router, @tanstack/react-query).
|
||
- [Словари из `/settings` пусты на чистой базе] → доска без `link_types` не покажет связей; экран показывает
|
||
«в настройках хаба нет link_types/kind_labels» вместо пустой доски.
|
||
- [Токен в `localStorage` доступен скриптам страницы] → сторонних скриптов нет, сборка своя; доступ только с Mac mini.
|
||
|
||
## Migration Plan
|
||
|
||
1. Merge → в основном checkout `git pull` → `make web` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`.
|
||
2. Открыть `http://127.0.0.1:8790/` → редирект на `/app/board`; вход уже есть (тот же ключ токена), раскладка — та же
|
||
(те же ключи `graph-…`).
|
||
3. Проверить: доска совпадает с прежней, «Обновить из Jira» проходит, «Задачи» → карточка → заметка сохраняется.
|
||
4. Откат: предыдущий `main` и перезапуск — `/` снова отдаёт `links.html`; раскладки в `localStorage` не меняются
|
||
новым приложением по формату, старая страница их читает.
|
||
|
||
## Open Questions
|
||
|
||
Нет.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| R-1 (design #13) | 2026-10-14 | Ответ владельца «да» на вопрос «добавить переход с карточки доски в карточку задачи или оставить на viewer-edit» понят как «добавить»; переход — отдельный значок «ⓘ», щелчок по ключу по-прежнему открывает Jira | вопрос и одобрение раздела пришли одним «да»; значок не меняет существующее поведение доски | убрать значок — одна правка компонента |
|
||
| R-2 (design #1) | 2026-10-14 | Менеджер пакетов — npm с `package-lock.json` и `npm ci` | npm уже стоит на Mac mini, ничего доставлять не нужно | переход на pnpm — замена lock-файла и команд в Makefile |
|
||
| R-3 (design #4) | 2026-10-14 | Файлы `assets` кэшируются навсегда (`immutable`), `index.html` — `no-cache` | имена файлов сборки содержат хэш; новая сборка видна без перезапуска и без сброса кэша | при ошибке — старый интерфейс до сброса кэша браузера |
|
||
| R-4 (design #17) | 2026-10-14 | Фикстуры доски синтетические, не снимок живого зеркала | в зеркале данные Jira банка, в личном репозитории их держать не надо | синтетика может не покрыть редкий случай живых данных — его ловит сверка на живом хабе при выкатке |
|
||
| R-5 (design #6) | 2026-10-14 | Прокси разработки перечисляет префиксы API явно, по умолчанию на тестовый хаб `:8791` | разработка не трогает живой хаб; явный список не перехватывает пути приложения | новый маршрут API надо дописать в прокси |
|
||
| R-6 (design #16) | 2026-10-14 | `updated_by` заметок из приложения — `"web"`; пустое поле отправляется как `null` | у хаба один клиентский токен, пользователя он не различает; `null` очищает поле так же, как MCP | автор в истории заметок неинформативен — до появления пользователей |
|
||
| R-7 (design #14) | 2026-10-14 | Поиск `q` в списке задач — с задержкой 300 мс | без задержки запрос на каждую букву | неудобство ввода — меняется одна константа |
|
||
| R-8 (design #4) | 2026-10-14 | Каталог сборки читается с диска на каждый запрос, без кэша в памяти хаба | так же работает `links.html`; `make web` без перезапуска хаба | лишнее чтение файла на запрос — несущественно при одном пользователе |
|
||
| R-9 (design #8) | 2026-10-14 | Экран входа не предлагает загрузку CSV/JSON без хаба; файл — только на доске после входа | разбор файла требует словарей из `/settings`; приложение отдаёт хаб, сценария «без хаба» нет | посмотреть CSV без токена нельзя — можно кэшировать словари в `localStorage` позже |
|
||
| R-10 (design #11) | 2026-10-14 | Двойной щелчок по уголку карточки меряет высоту по содержимому при новой ширине (CW в пределах колонки), а не при прежней, как `links.html` (побочный эффект порядка вызовов в строке 750) | намерение «размер по содержимому» — высота под ширину, которую ставит то же действие; golden-тесты этот путь не покрывают | на карточке с ручной шириной высота после двойного щелчка может отличаться от старой страницы на строку заголовка |
|
||
| R-11 (design #12) | 2026-10-14 | «Строка состояния» доски — два элемента, как у `links.html`: подпись источника (`label`: «Хаб · данные на …», «… (из файла)», на месте `fileName`) и одно текущее событие (`status`: загрузка, пакет, ошибки — на месте `hubState`); событие пишет один автомат состояний, перезаписи сообщений (M-5) нет | так выглядела прежняя доска, а M-5 — про гонку сообщений, не про число элементов; формулировка требования «Источник данных доски» уточнена | если нужна буквально одна строка — объединить два элемента в `Toolbar` |
|
||
| R-12 (design #18) | 2026-10-14 | Строка `Co-Authored-By` в коммитах исполнения — имя модели реального автора (haiku/sonnet), а не «Claude Opus 5.5» из плана; амендов нет | коммиты делают субагенты haiku/sonnet, системная инструкция атрибуции требует их имя | неверная подпись в трейлере коммитов. Пересмотрено основной сессией: с Task 5 трейлер — по плану (Opus 5.5); коммиты Tasks 1–4 не переписываются |
|
||
| R-13 (design #19) | 2026-10-14 | `fromJson` сам нормализует копию фильтров: при `mode === "linked"` ставит `neighbors = true`, как `applyJsonSettings` старой страницы (golden-эталон `old-board` снят после этой мутации); обработка `mode === "all"` остаётся за Task 9 | эталон нельзя править руками, а побочный эффект старой страницы — часть наблюдаемого поведения импорта | если нормализация лишняя — убрать две строки в `fromJson` и перегенерировать эталон |
|
||
| R-14 (design #20) | 2026-10-14 | В golden-тесте раскладки `dateLabels` сравниваются только при `byDate`; эталон не правится и генератор Task 5 не переделывается | в сценарии `saved-folded` (byDate=false) эталон хранит 7 подписей дат от предыдущего сценария: заглушка DOM генератора не очищает полосу дат — артефакт генератора, не поведение страницы | при `byDate=false` приложение, возможно, показывает лишние подписи — это ловит компонентный тест холста (Task 11) и сверка в Task 15 |
|
||
| R-15 (design #21) | 2026-10-14 | Ожидания теста `BoardCanvas` из плана выправлены по golden-эталону (4 дорожки, 9 карточек, 6 стрелок; заглушка 9500 и «Нет в зеркале» видны только при who=Иванов+neighbors; «Ниже» у Гудвилла даёт to=1); код компонентов — как в плане | эталон получен запуском настоящей `links.html@3931a88`, он — источник истины о поведении; цифры в плане были написаны по памяти | если эталон не отражает желаемого поведения — менять фикстуры и генератор, а не тест |
|
||
| R-16 (design #22) | 2026-10-14 | В `board.css` слой измерения получает `height:auto` (`.measure .card{position:static;height:auto}`), в отличие от CSS из плана | у `.card` фиксированные 120px; оригинал ставил `height:auto` перед измерением (links.html ~826), без этого авто-высота карточек не работает (I-1 ревью Task 11) | неверная раскладка высот; проверка реальной геометрии — e2e Task 15 |
|
||
| R-17 (design #23) | 2026-10-14 | Исполнитель Task 12 исправил в рамках задачи два дефекта прежних задач: `useBoardSource` отдаёт стабильную `emptyModel` через `useMemo` (иначе `useBoardPrefs` сбрасывает выбор при каждой новой модели → «Too many re-renders»), `shell/theme.ts readTheme` возвращает значение по умолчанию при пустом хранилище (иначе «Тема: undefined») | без правок страница доски не работала; отдельная задача ради двух строк — лишний круг | бесконечный рендер или «undefined» в меню темы; правки проверяются ревью Task 12 |
|
||
| R-18 (design #24) | 2026-10-14 | `setPointerCapture` в `BoardCanvas` ставится на нажатом элементе (`t`), а не на карточке (коммит d6fe046) — двойной щелчок по уголку `.rsz` снова сбрасывает размер | в Chromium capture на карточке отправляет `dblclick` на карточку; дефект воспроизведён и в старой `/links` (ширина 240→320→320), то есть унаследован; без правки e2e «растяжка…» недостижим | поведение перетаскивания/растяжки отличается от `links.html` в крайнем случае; откат — revert d6fe046 |
|