Веб-приложение хаба на React: доска связей и задачи (#21) #27

Merged
aep merged 31 commits from feat/21-web-app into main 2026-10-14 23:19:56 +03:00
Owner

Закрывает #21, закрывает #20.

Веб-приложение хаба на React + TypeScript + Vite (web/), сборка — в src/jira_hub/web/app/ (не в git). Хаб отдаёт его под /app/, а / и /links перенаправляет на /app/board. Файл links.html удалён.

Что внутри

  • Оболочка. Вход по клиентскому токену (jira-hub-token в localStorage, как у старой доски), разделы «Доска» и «Задачи», «Сменить токен», страница «не найдено», сообщение об ошибке отрисовки.
  • Доска (/app/board) перенесена с links.html@3931a88 один в один. Логика (модель, видимая выборка, критический путь, проблемы, раскладка) — чистые TS-модули, их сверяют golden-тесты с эталонами, снятыми со старой страницы. Отрисовка — React + SVG. Раскладки и фильтры читаются из прежних ключей localStorage. Добавлен значок «ⓘ»: он открывает карточку задачи в приложении.
  • Загрузка данных доски закрывает M-1…M-5 из #20: явный источник «хаб или файл», одна кнопка «Обновить из Jira» и один опрос пакета, подхват незавершённого пакета, остановка опроса на 4xx и после 5 ошибок подряд, одно событие в шапке. M-7 — тест mirror.board(own=False).
  • Задачи (/app/issues, /app/issues/{key}): список с фильтрами в адресе, порции по 50. Карточка: поля, описание, связи, последние команды. Правка служебных заметок: PUT уходит только с изменёнными полями, при уходе с несохранённой правкой — подтверждение.
  • Makefile: make web собирает приложение, make check запускает pytest, tsc, eslint и vitest. Есть тестовый хаб web/scripts/test-hub.sh (:8791) и e2e на Playwright.
  • Спеки: новая web-app; в hub-api — MODIFIED «Процесс и доступ», ADDED «Раздача веб-приложения», REMOVED «Страница доски связей».
  • В ветку влит main (agent-self-update #22), конфликты в CLAUDE.md, ARCHITECTURE.md, api.py и test_api.py разрешены с сохранением обеих сторон.

Проверки

  • make check: pytest 249 passed, tsc и eslint чисто, vitest 18 файлов / 129 тестов.
  • cd web && npm run e2e на тестовом хабе :8791: 12 passed (вход, доска, геометрия, задачи).
  • Ручной проход в Playwright на :8791. Проверено:
    • /links → /app/board;
    • неверный и устаревший токен;
    • доска: «по датам», «Проблемы», таблица связей, тёмная тема;
    • «ⓘ» → карточка; сохранение заметки (тело {rest_text, updated_by: "web"}), подтверждение ухода;
    • фильтры в адресе и после перезагрузки;
    • 404 задачи и страницы;
    • остановка хаба → «хаб не отвечает — показаны прежние данные».
  • Живой хаб :8790 не затрагивался. На реальных данных доску нужно сверить пос��е выкатки, потому что фикстуры синтетические (R-4).

Rulings I made

Номер решения в openspec/changes/web-app/design.md указан рядом. R-1…R-11 — с этапа спеки и плана (в design.md).

  • R-12 (design #18): трейлер коммитов Tasks 1–4 называет реального автора (Haiku/Sonnet). Основная сессия пересмотрела: с Task 5 трейлер Opus 5.5 по инструкции атрибуции, коммиты 1–4 не переписывались. Цена: подпись в трейлере.
  • R-13 (design #19): fromJson нормализует копию фильтров (mode=linked → neighbors=true), как старая страница; mode="all" обрабатывается в хранилище. Цена: две строки в fromJson и перегенерация эталона.
  • R-14 (design #20): в golden-тесте раскладки dateLabels сравниваются только при byDate: в генераторе это артефакт заглушки DOM. Цена: лишние подписи при byDate=false; проверено, что DatesBar рисуется только при byDate.
  • R-15 (design #21): ожидания теста BoardCanvas выправлены по golden-эталону, а не по цифрам плана. Цена: если эталон не отражает желаемого, менять фикстуры, а не тест.
  • R-16 (design #22): .measure .card { position: static; height: auto } вопреки CSS плана. Цена: неверная раскладка высот; e2e geometry это ловит.
  • R-17 (design #23): приняты правки Task 12 в файлах прежних задач: стабильная emptyModel через useMemo и значение темы по умолчанию. Цена: бесконечный рендер или «Тема: undefined»; финальное ревью подтвердило корректность.
  • R-18 (design #24): setPointerCapture ставится на нажатом элементе, поэтому двойной щелчок по уголку снова возвращает размер (дефект унаследован из links.html). Отклонение от старой страницы внесено в спеку. Цена: откат — revert d6fe046.
  • Без записи в design.md: правка CLAUDE.md в Task 16 (строка о текущем change, пункт «Веб-приложение — web/»).

Deferred (уйдут в follow-up issue при закрытии)

Маркер Суть
T1.1 /app/assets/%00 → 500 вместо 404
T1.4 тест Content-Type не покрывает svg/png/ico/woff2/map
T2.1 canon.ts: localeCompare зависит от локали
T2.2 шрифты с fonts.googleapis.com (унаследовано из links.html, есть системные запасные; для работы без интернета держать локально)
T3.2 4xx на экране входа показан как «Хаб не отвечает»
T3.3 тавтологичный тест 401; queryClient/queries без тестов
T3.4 token.test не проверяет removeItem при закрытом хранилище
T3.5 запоздавший 401 от старого токена разлогинит после нового входа
T3.6 нет интеграционного теста AuthError → LoginScreen
T4.1 тест «Задачи и обратно» без обратного перехода
T4.2 тест NotFound не проверяет ссылки
T4.3 App.Layout нужен только тесту
T4.4 два импорта из @testing-library/react
T4.5 theme.test без очистки атрибутов
T5.1 golden layout.clamp: 87 из 88 значений «unset»
T6.3 util.ts без прямых тестов
T7.1 initialWho считает whoItems дважды
T9.1 colWidths() не проверяет, что значения — числа
T9.3 нет тестов toggleLane/moveLane/resetLayout/exportFileName/mode=all
T9.4 порядок импортов fromJson.ts
T10.1 лишний GET /batches после завершения пакета
T10.3 «Перечитать» и загрузка CSV сбрасывают событие «пакет выполняется»
T10.4 ошибка снимка при показанном файле → «показаны прежние данные»; hasData:true для JSON
T10.5 тест M-1 «поздний ответ» может пройти вакуумно
T10.6 подхват пакета проверяет автора, но не тип refresh
T11.1 memo(Card) неэффективен
T11.2 requestAnimationFrame в DatesBar не отменяется
T11.3 нет теста перетаскивания дорожек
T11.4 слабые юнит-тесты I-2/I-3 (настоящая проверка — в e2e geometry)
T12.1 onOpen() вызывается внутри updater setOpen
T12.2 writeFlag/writeHidden вызываются внутри updater
T12.3 useLayoutEffect без deps, --head-h не сбрасывается
T12.4 role=listbox без option в MultiSelect
T13.1 значение фильтра из адреса вне вариантов списка
T14.2 проверки гонок в тестах заметки на таймерах 20 мс
T14.3 статичный id у datalist
T15.1 geometry: проверка высоты карточек повторяет формулу
T15.2 тест растяжки без expect.poll, литерал 240
T15.4 порт 8791 продублирован
T15.6 хрупкость окружения e2e
T16.1 дубль import urllib.* в test_api.py
M-3 состояние доски теряется при переходе в карточку и обратно
M-5 wheel пакета не содержит сборку приложения
O-1 npm audit: 1 high в source-map-js, только dev-зависимость (--omit=dev — 0)
O-10 карточка задачи: ссылки цвета браузера по умолчанию, не темы
O-11 карточка задачи: пустая таблица «Последние команды» без текста «команд нет»

Исправлены: I1 (Task 3) c124e08; I-1…I-3 (Task 11) 089c4fb; T8.1 5f012db; T15.F1 d6fe046; T10.2, T13.2, T14.1, T15.5, M-1, M-2, M-4 d608a16; F-1 60c30a9. Финальное ревью (opus) не нашло ни Critical, ни Important.

Выкатка после merge

git pull → make web → launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub → открыть http://127.0.0.1:8790/app/board.

🤖 Generated with Claude Code

Закрывает #21, закрывает #20. Веб-приложение хаба на React + TypeScript + Vite (`web/`), сборка — в `src/jira_hub/web/app/` (не в git). Хаб отдаёт его под `/app/`, а `/` и `/links` перенаправляет на `/app/board`. Файл `links.html` удалён. ## Что внутри - **Оболочка.** Вход по клиентскому токену (`jira-hub-token` в `localStorage`, как у старой доски), разделы «Доска» и «Задачи», «Сменить токен», страница «не найдено», сообщение об ошибке отрисовки. - **Доска (`/app/board`)** перенесена с `links.html@3931a88` один в один. Логика (модель, видимая выборка, критический путь, проблемы, раскладка) — чистые TS-модули, их сверяют golden-тесты с эталонами, снятыми со старой страницы. Отрисовка — React + SVG. Раскладки и фильтры читаются из прежних ключей `localStorage`. Добавлен значок «ⓘ»: он открывает карточку задачи в приложении. - **Загрузка данных доски** закрывает M-1…M-5 из #20: явный источник «хаб или файл», одна кнопка «Обновить из Jira» и один опрос пакета, подхват незавершённого пакета, остановка опроса на 4xx и после 5 ошибок подряд, одно событие в шапке. M-7 — тест `mirror.board(own=False)`. - **Задачи (`/app/issues`, `/app/issues/{key}`)**: список с фильтрами в адресе, порции по 50. Карточка: поля, описание, связи, последние команды. Правка служебных заметок: `PUT` уходит только с изменёнными полями, при уходе с несохранённой правкой — подтверждение. - **`Makefile`**: `make web` собирает приложение, `make check` запускает pytest, tsc, eslint и vitest. Есть тестовый хаб `web/scripts/test-hub.sh` (:8791) и e2e на Playwright. - **Спеки**: новая `web-app`; в `hub-api` — MODIFIED «Процесс и доступ», ADDED «Раздача веб-приложения», REMOVED «Страница доски связей». - В ветку влит `main` (agent-self-update #22), конфликты в `CLAUDE.md`, `ARCHITECTURE.md`, `api.py` и `test_api.py` разрешены с сохранением обеих сторон. ## Проверки - `make check`: pytest 249 passed, tsc и eslint чисто, vitest 18 файлов / 129 тестов. - `cd web && npm run e2e` на тестовом хабе :8791: 12 passed (вход, доска, геометрия, задачи). - Ручной проход в Playwright на :8791. Проверено: - `/links` → `/app/board`; - неверный и устаревший токен; - доска: «по датам», «Проблемы», таблица связей, тёмная тема; - «ⓘ» → карточка; сохранение заметки (тело `{rest_text, updated_by: "web"}`), подтверждение ухода; - фильтры в адресе и после перезагрузки; - 404 задачи и страницы; - остановка хаба → «хаб не отвечает — показаны прежние данные». - Живой хаб :8790 не затрагивался. На реальных данных доску нужно сверить пос��е выкатки, потому что фикстуры синтетические (R-4). ## Rulings I made Номер решения в `openspec/changes/web-app/design.md` указан рядом. R-1…R-11 — с этапа спеки и плана (в design.md). - R-12 (design #18): трейлер коммитов Tasks 1–4 называет реального автора (Haiku/Sonnet). Основная сессия пересмотрела: с Task 5 трейлер Opus 5.5 по инструкции атрибуции, коммиты 1–4 не переписывались. Цена: подпись в трейлере. - R-13 (design #19): `fromJson` нормализует копию фильтров (`mode=linked` → `neighbors=true`), как старая страница; `mode="all"` обрабатывается в хранилище. Цена: две строки в `fromJson` и перегенерация эталона. - R-14 (design #20): в golden-тесте раскладки `dateLabels` сравниваются только при `byDate`: в генераторе это артефакт заглушки DOM. Цена: лишние подписи при `byDate=false`; проверено, что `DatesBar` рисуется только при `byDate`. - R-15 (design #21): ожидания теста `BoardCanvas` выправлены по golden-эталону, а не по цифрам плана. Цена: если эталон не отражает желаемого, менять фикстуры, а не тест. - R-16 (design #22): `.measure .card { position: static; height: auto }` вопреки CSS плана. Цена: неверная раскладка высот; e2e geometry это ловит. - R-17 (design #23): приняты правки Task 12 в файлах прежних задач: стабильная `emptyModel` через `useMemo` и значение темы по умолчанию. Цена: бесконечный рендер или «Тема: undefined»; финальное ревью подтвердило корректность. - R-18 (design #24): `setPointerCapture` ставится на нажатом элементе, поэтому двойной щелчок по уголку снова возвращает размер (дефект унаследован из `links.html`). Отклонение от старой страницы внесено в спеку. Цена: откат — revert d6fe046. - Без записи в design.md: правка `CLAUDE.md` в Task 16 (строка о текущем change, пункт «Веб-приложение — web/»). ## Deferred (уйдут в follow-up issue при закрытии) | Маркер | Суть | |---|---| | T1.1 | `/app/assets/%00` → 500 вместо 404 | | T1.4 | тест Content-Type не покрывает svg/png/ico/woff2/map | | T2.1 | `canon.ts`: `localeCompare` зависит от локали | | T2.2 | шрифты с fonts.googleapis.com (унаследовано из links.html, есть системные запасные; для работы без интернета держать локально) | | T3.2 | 4xx на экране входа показан как «Хаб не отвечает» | | T3.3 | тавтологичный тест 401; `queryClient`/`queries` без тестов | | T3.4 | `token.test` не проверяет `removeItem` при закрытом хранилище | | T3.5 | запоздавший 401 от старого токена разлогинит после нового входа | | T3.6 | нет интеграционного теста AuthError → LoginScreen | | T4.1 | тест «Задачи и обратно» без обратного перехода | | T4.2 | тест NotFound не проверяет ссылки | | T4.3 | `App.Layout` нужен только тесту | | T4.4 | два импорта из `@testing-library/react` | | T4.5 | `theme.test` без очистки атрибутов | | T5.1 | golden `layout.clamp`: 87 из 88 значений «unset» | | T6.3 | `util.ts` без прямых тестов | | T7.1 | `initialWho` считает `whoItems` дважды | | T9.1 | `colWidths()` не проверяет, что значения — числа | | T9.3 | нет тестов `toggleLane`/`moveLane`/`resetLayout`/`exportFileName`/`mode=all` | | T9.4 | порядок импортов `fromJson.ts` | | T10.1 | лишний `GET /batches` после завершения пакета | | T10.3 | «Перечитать» и загрузка CSV сбрасывают событие «пакет выполняется» | | T10.4 | ошибка снимка при показанном файле → «показаны прежние данные»; `hasData:true` для JSON | | T10.5 | тест M-1 «поздний ответ» может пройти вакуумно | | T10.6 | подхват пакета проверяет автора, но не тип refresh | | T11.1 | `memo(Card)` неэффективен | | T11.2 | `requestAnimationFrame` в `DatesBar` не отменяется | | T11.3 | нет теста перетаскивания дорожек | | T11.4 | слабые юнит-тесты I-2/I-3 (настоящая проверка — в e2e geometry) | | T12.1 | `onOpen()` вызывается внутри updater `setOpen` | | T12.2 | `writeFlag`/`writeHidden` вызываются внутри updater | | T12.3 | `useLayoutEffect` без deps, `--head-h` не сбрасывается | | T12.4 | `role=listbox` без `option` в MultiSelect | | T13.1 | значение фильтра из адреса вне вариантов списка | | T14.2 | проверки гонок в тестах заметки на таймерах 20 мс | | T14.3 | статичный id у `datalist` | | T15.1 | geometry: проверка высоты карточек повторяет формулу | | T15.2 | тест растяжки без `expect.poll`, литерал 240 | | T15.4 | порт 8791 продублирован | | T15.6 | хрупкость окружения e2e | | T16.1 | дубль `import urllib.*` в `test_api.py` | | M-3 | состояние доски теряется при переходе в карточку и обратно | | M-5 | wheel пакета не содержит сборку приложения | | O-1 | `npm audit`: 1 high в `source-map-js`, только dev-зависимость (`--omit=dev` — 0) | | O-10 | карточка задачи: ссылки цвета браузера по умолчанию, не темы | | O-11 | карточка задачи: пустая таблица «Последние команды» без текста «команд нет» | Исправлены: I1 (Task 3) c124e08; I-1…I-3 (Task 11) 089c4fb; T8.1 5f012db; T15.F1 d6fe046; T10.2, T13.2, T14.1, T15.5, M-1, M-2, M-4 d608a16; F-1 60c30a9. Финальное ревью (opus) не нашло ни Critical, ни Important. ## Выкатка после merge `git pull` → `make web` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub` → открыть `http://127.0.0.1:8790/app/board`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 31 commits 2026-10-14 23:12:36 +03:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Типы ответов хаба, клиент с разбором ошибок (AuthError, HubDownError,
ApiError), хранилище токена с подпиской, экран входа с проверкой токена
до сохранения, QueryClient с выходом на экран входа при 401, хуки запросов.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Сохранённый в localStorage токен с кириллицей, пробелом или переводом строки
давал TypeError у fetch, который превращался в HubDownError. Теперь call()
проверяет TOKEN_RE до запроса и бросает AuthError.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Заглушка модели в useBoardSource стабильна между рендерами, readTheme не возвращает null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
В Chromium dblclick после setPointerCapture на карточке уходил на карточку, а не на .rsz;
то же поведение у links.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ARCHITECTURE.md
#	CLAUDE.md
#	src/jira_hub/api.py
#	tests/test_api.py
aep merged commit e5719c9c53 into main 2026-10-14 23:19:56 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: aep/sdlc-hub#27
No description provided.