28 KiB
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
- Отдельный 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. - Стек. 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— точные, без^. - Приложение под
/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; неизвестный путь внутри приложения — страница «не найдено» со ссылками на разделы. - Раздача в хабе (
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(сборка не выполнена) — 503text/htmlс текстом «Веб-приложение не собрано: выполнитеmake webв каталоге jira-hub и перезапустите хаб» — на любой/app…; GET /иGET /links— 302Location: /app/board;PAGEиlinks.htmlудаляются. Файлы читаются с диска на каждый запрос (как сейчасlinks.html): новая сборка видна без перезапуска.
- Сборка не в 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/путь, подменяемый в тесте). - Разработка.
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). - Один клиент 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). - Вход. Токен —
localStorage["jira-hub-token"](тот же ключ, что уlinks.html) и копия в памяти на случай закрытого хранилища. Нет токена или любой запрос ответилAuthError→ экран входа поверх приложения: панели и страницы не показываются, запросы не идут (enabled: false), поле токена не трогается фоновыми событиями (M-6). После ввода — проверкаGET /settings; при успехе токен сохраняется, кэш запросов сбрасывается. «Сменить токен» в оболочке очищает токен и показывает экран входа. Загрузки файла CSV/JSON с экрана входа, как уlinks.html, нет: разбору нужны словари из/settings, а приложение отдаёт сам хаб, так что без хаба его не открыть; файл загружается на доске после входа. - Оболочка. Верхняя панель: название «Хаб Jira», ссылки «Доска», «Задачи», «Сменить токен». Граница ошибок
(
ErrorBoundary) на уровне страницы: неожиданное исключение отрисовки → сообщение и кнопка «Перезагрузить», панель остаётся. - Доска: модули без 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-фикстурах.
- Доска: состояние и компоненты. Модель и источник — в
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 карточки. - Доска: источник данных и обновление (закрывает 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)сrefetchInterval3 с до терминального статуса пакета →invalidate(/board). Кнопка выключена, пока мутация в пути или есть активный пакет (M-2); опрос пакета — один запрос по ключу["batch", id], второго опроса не бывает. Незавершённый пакетviewerищется (GET /batches) при каждой успешной загрузке/board, а не только при первой (M-3). Опрос останавливается на 404 сразу и после 5 ошибокHubDownErrorподряд — с сообщением в строке состояния и снова включённой кнопкой (M-4).StatusLineпоказывает одно состояние из перечня: «загрузка», «данные хаба на <время>», «из файла <имя>», «пакет #N выполняется», «пакет #N выполнен», «пакет #N: ошибка <текст>», «хаб не отвечает — показаны прежние данные», «агент офлайн — пакет поставлен»; ошибки разбора файла — отдельным текстом «файл не разобран: <причина>», не «хаб не отвечает» (M-5). - Доска: «Открыть карточку». В заголовке карточки (не у заглушек) рядом с ключом — значок-ссылка «ⓘ» на
/app/issues/{key}(переход внутри приложения). Ключ по-прежнему открывает задачу в Jira, перетаскивание и выделение текста не меняются. - Задачи: список
/app/issues. Таблица: ключ, тема, тип, статус Jira, исполнитель, спринт, срок (planned_end, иначеduedate), эпик (имя из снимка/board, иначе ключ), вид работы, статус по факту, «сосед». Фильтры — параметрыGET /issues, состояние фильтров — в query-строке адреса (useSearchParams); поискqприменяется с задержкой 300 мс. Варианты выпадающих списков (спринты, исполнители, статусы Jira, эпики, метки, виды работы, статусы по факту) — изuseBoard(), без новых маршрутов. Порции по 50 (useInfiniteQuery,offset), кнопка «Показать ещё» видна, пока последняя порция полная (50 строк). Ключ — ссылка на карточку. - Задачи: карточка
/app/issues/:key. Поля задачи, «открыть в Jira», описание — текст сwhite-space: pre-wrapбез разбора разметки Jira; связи «исходящие»/«входящие» с подписью типа (логический тип изlink_types, иначе имя Jira), ключ — ссылка на карточку; 10 последних команд: id, пакет, тип, статус, время, ошибка. 404 → «задачи нет в зеркале» со ссылкой на список. - Задачи: заметки. Форма в карточке: «вид работы» — выпадающий список из значений
kind_labels(плюс текущее значение, если его там нет, и «—» = пусто); «статус по факту» — поле с подсказками (datalist) из ключейstatus_labels; «сделано», «осталось», «заметка владельца» — многострочные поля. «Сохранить» активна при изменениях и отправляет только изменённые поля (PUT /issues/{key}/note,updated_by: "web"); пустое поле отправляется какnull. Успех → обновлённая заметка в кэше карточки и списка, «сохранено <время>, <кто>». 422 → текстdetailу формы, введённое не теряется. Уход со страницы с несохранёнными правками → подтверждение (useBlockerиbeforeunload). - 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с сортировкой ключей. - Проверки. 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
- Merge → в основном checkout
git pull→make web→launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub. - Открыть
http://127.0.0.1:8790/→ редирект на/app/board; вход уже есть (тот же ключ токена), раскладка — та же (те же ключиgraph-…). - Проверить: доска совпадает с прежней, «Обновить из Jira» проходит, «Задачи» → карточка → заметка сохраняется.
- Откат: предыдущий
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 |