# Архитектура Хаб моста Jira: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает. ## Модули | Модуль | Ответственность | Зависит от | |---|---|---| | `paths` | каталог данных (`JIRA_HUB_HOME`), пути базы и `backups/`, права 0700 (`secure_dir`) | stdlib | | `db` | схема SQLite (v6: + `issue_attachment`, `attachment_file`), миграции по `schema_version` (копия базы перед миграцией), `connect`, время Jira → UTC (`jira_utc`) | `backup`, `paths`, `projects` (ленивый импорт в миграции v3→v4) | | `settings` | настройки (`setting`), токены, темп, перенос из `config.example.json` | `db` | | `projects` | проекты-метки: таблица `project` (метка, название, `board_id`, `board_type`), проверка метки, базовая JQL из меток проектов (`base_jql`), доски (`boards`), миграция схемы v3→v4 (`migrate_v3`) | `db`, `settings` | | `attachments` | вложения для интерфейсов: ссылки на них в вики-разметке текста (`ref_names`, `resolve`), формат размера (`fmt_size`), состояние `ready`/`fetching`/`failed`/`none` по `attachment_file`, хранилищу и последней `fetch_attachment` (`state`, `get`) | `files` | | `mirror` | зеркало задач, связей, комментариев (время комментария в UTC — `created_at`) и списка вложений `issue_attachment` (схема v6; `check_attachments`, пишет `apply_state`; `attachment_file` ему не принадлежит), разрез по проекту (свои, соседи по связям, эпики своих), `apply_state` (с проверками `check_comments`, `check_attachments`), заметки, фильтры, снимок доски `board`, карточка `get_issue` (с историей задачи из `history`) | `db`, `history`, `attachments` | | `history` | журнал изменений задач `issue_change` (схема v5): проверка и запись пачек агента (`check_changes`, `record_changes`, логическое имя поля `logical_field`), параметр `history` для выдачи `refresh` (`delivery_param`: отметка — последний полный прогон с `sync_run.history_at`, минус 15 минут; стартовая JQL `backfill_jql`), признак принятого журнала `accepted`, лента `feed` (журнал + новые комментарии по `issue_comment.created_at`) и история задачи `issue_changes` | `db`, `settings`, `projects` | | `commands` | контракт типов команд, валидация, временные ключи, оценка, подтверждение по состоянию | `mirror` | | `queue` | пакеты, проверки постановки (`_check_setup`: `bug` без настройки, файл вложения; `_check_project`: проверки пакета проекта — метка, чужие метки, задачи вне проекта; `_assign_boards`: `board_id` проекта у `ensure_sprint`), выдача (`claim`, `size` у `ensure_attachment` и `boards` у `refresh` — `deliver_params`), отчёты (для `fetch_attachment` в `done` — проверка файла в хранилище и запись `attachment_file`), `request_fetch` (постановка скачивания вложения в одной транзакции, `TooLarge`, `AgentTooOld`; пакет без хвостового `refresh` и без резервной копии), отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `files`, `projects`, `attachments`, `db` | | `backup` | копии файла базы, ротация | `paths` | | `files` | хранилище файлов вложений `<данные>/files/` (каталог — параметр, `Hub.files_dir`): `put` (атомарно, одна копия на содержимое, mtime при повторе, предел 10 МБ, пустой — отказ, кроме `allow_empty` для файла от агента), `touch` (mtime при выдаче содержимого), `sweep_downloads` (каталоги копий MCP `downloads//`, старше 30 дней), `path`/`exists`/`size`/`read` (только sha из 64 hex нижнего регистра), `sweep` (старше 30 дней, кроме файлов `ensure_attachment` в `queued`/`claimed`) | `paths` | | `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` (автообновление и уборка `files/` раз в сутки); маршруты файлов `POST /files`, `GET /agent/files/{sha256}`, `POST /agent/files` (от агента, пустой допустим; каталог `Hub.files_dir`, 0700 при старте); вложения `GET /attachments/{id}`, `POST /attachments/{id}/fetch` (`queue.request_fetch`: `TooLarge` → 413, `AgentTooOld` → 409, нет → 404 `no_attachment`), `GET /attachments/{id}/content` (`Content-Disposition: filename*=UTF-8''…`, `nosniff`, `files.touch`); уборка раз в сутки трогает и `Hub.downloads_dir`; раздача веб-приложения `/app…` (каталог `web/app` пакета, без токена) и перенаправление `/`, `/links`; снимок `GET /board`; маршруты `/projects` | `queue`, `mirror`, `projects`, `settings`, `backup`, `files`, `attachments`, `relay`, `release` | | `relay` | реле общей папки (настройка `relay_dir`): поток в `serve`, файлы `req/` → свой HTTP (адрес `serve`, для `0.0.0.0` — `127.0.0.1`) → `resp/`; агент с `hub_url = folder:<путь>` ходит через него; HTTP-обработчики о нём не знают | `settings` | | `release` | опубликованная версия агента: `release/agent.py` + `manifest.json` в каталоге данных, `publish` (только вверх; запись с fsync, прежний файл на время записи — `agent.py.prev`, при сбое возвращается), `current` (проверка манифеста, предупреждение один раз на ошибку), `load` (с проверкой sha256 и UTF-8), подсказка `hint` для `agent_status` | `paths` | | `client` | HTTP-клиент хаба | `db`, `paths`, `settings` (только для `from_home`) | | `mcp_server` | инструменты MCP по stdio; привязка сессии к проекту (`jira-hub mcp --project <метка>`), инструменты `projects` и `project_create` (фрагмент `.mcp.json`), лента `changes` (`GET /changes`, по умолчанию — с начала суток); `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета; `issue`/`comment` показывают вложения, инструмент `attachment` ставит скачивание (`POST /attachments/{id}/fetch`), опрашивает `GET /attachments/{id}` и кладёт копию в `<данные>/downloads//<имя>` (0700, через временный файл) | `client` (`get_bytes` — тело ответа как есть), `commands`, `paths`, `files` (только `MAX_SIZE`), `attachments` (только `fmt_size`) | | `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` | | `cli` | `init`, `serve`, `import`, `mcp`, `tokens`, `agent publish` | `api`, `mcp_server`, `importer`, `release`, `settings`, `db`, `paths` | `web/` — веб-приложение хаба (React + TypeScript + Vite): вход по клиентскому токену, страницы «Доска» (`/app/board`), «Задачи» (`/app/issues`) и «Лента» (`/app/feed`: журнал изменений `GET /changes` за период; `web/src/feed/` — чистые функции `events.ts` (период, виды, текст события, группировка) и `EventList.tsx`, общий с блоком «История» карточки); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует страницу по адресу из `location.state.background` (`useRoutes` с явным адресом) и `issues/IssueModal.tsx` поверх неё. Собирается `make web` в `src/jira_hub/web/app/`; `api` отдаёт сборку без токена (`/app/assets/*` — файлы, прочие `/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на `/app/board`. Данные — только через HTTP API с клиентским токеном; `mirror.board` отдаёт зеркало без преобразований, перевод в модель доски — в `web/src/board/model`. Логика доски — чистые модули `web/src/board/{model,analysis,layout}` и `storage.ts` (ключи `localStorage` прежней страницы; у проекта AWPCA — прежние, у остальных с суффиксом `@<метка>`; набор ключей — состояние модуля, выставляется при монтировании страницы), отрисовка — `web/src/board/components`. Вид — дизайн-система AURA: токены цвета, шрифта, отступов и теней (светлая и тёмная темы) — `web/src/styles/tokens.css`, единственное место с литералами цвета; базовые компоненты (`Button`, `Notice`, `Dialog`, `Field`, `Panel`, меню и др.) — `web/src/ui/`, остальные стили подключаются к токенам по семантическим именам. Оболочка — `web/src/shell/Layout.tsx`: grid из боковой панели `Sidebar.tsx` (закреплена у левого края, сворачивается в полосу значков — ключ `jira-hub-sidebar`; на экранах уже 640 px — строка с «Меню» и выезжающая панель) и страницы; прокрутка — у окна. Обновление данных из Jira (кнопка в боковой панели, отслеживание пакета) — `web/src/shell/refresh.ts` (провайдер в `App.tsx`); доска отдаёт наружу только источник и события снимка. Цепочки: `api → queue → commands/mirror/settings/projects/history → db`; `mirror → history → projects/settings`; `mirror → attachments → files`; `db → projects` (только миграция, ленивый импорт); `projects → settings`; `db → backup → paths`, `db → paths`; `files → paths`; `api → relay → settings`; `mcp_server → client → HTTP API`; `importer → mirror/settings`. ## Потоки данных - Запись в Jira: клиент (`mcp_server`/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`. - Вложение: файл → `POST /files` → `files/` → `ensure_attachment {key, file, filename}` в пакете (`queue._check_setup` проверяет, что файл есть) → `claim` (хаб дописывает `size`, в базе `params` без него) → агент `GET /agent/files/{sha256}` → Jira; уборка `files/` — из `tick` раз в сутки. - Создание проекта: `POST /projects` (веб или `project_create`) → `projects.create` и пакет «Новый проект <метка>» (`refresh` по общей JQL, `batch.project = NULL`) в одной транзакции → `claim` (хаб дописывает в `refresh` доски проектов, `boards`) → агент определяет тип доски → отчёт `refresh` пишет `board_type` → `ensure_sprint` проекта получает `board_id`. - Чтение Jira: команда `refresh` → агент → `POST /agent/state` → `mirror.apply_state`. - Скачивание вложения: `queue.request_fetch` (одна транзакция: состояние `attachments.state` + пакет `fetch_attachment {key, attachment_id}`) → `claim` (без копии базы) → агент → `POST /agent/files` → результат `{file, size}` → `report_result` проверяет файл в `files/` и пишет `attachment_file`; нет файла или чужой размер — команда `failed`. - Журнал изменений: выдача `refresh` (`queue.deliver_params` дописывает `history` — `since` и/или стартовую `jql`) → агент v13 после состояния читает changelog Jira → `POST /agent/history` → `history.record_changes` (повторы отбрасываются по `(history_id, seq)`) → отчёт `refresh` с `result.history` → `sync_run.history_at` у полного прогона (новая отметка) → `GET /changes`, `GET /issues/{key}.changes`, MCP `changes`. - Агент за VPN: агент → файлы в общей папке (`relay_dir`) → `relay` → HTTP хаба → ответ файлом. - Самообновление агента: `jira-hub agent publish` → `release/` → `claim.agent_release` → агент скачивает `GET /agent/release`; состояние обновления агент шлёт в `claim`, хаб держит в памяти (вместе с признаком `agent_reported` — был ли `claim` с запуска хаба) и отдаёт в `GET /agent`. - Токены: агентский — только `/agent/*`, клиентский — остальное; MCP берёт клиентский токен из базы через `HubClient.from_home`. ## Границы - Зеркало (включая `issue_attachment`) пишет только `mirror.apply_state` по запросу агента (и заметки через `PUT /issues/{key}/note`); из очереди и API задач напрямую не правят. - Журнал `issue_change` пишет только `history.record_changes` по `POST /agent/history`; API и MCP его только читают. - Хаб не знает пароль Jira и не обращается к Jira; `init` не переносит `auth`, `base_url`, `verify_ssl`, `ca_bundle`. - `queue` не создаёт команд сам: повтор после failed — новый пакет. Выданная без отчёта команда довыдаётся при следующем `claim` (до 3 выдач, затем failed). - `mcp_server` не импортирует `queue`/`mirror`/`db`/`projects`/`history` напрямую и ходит только через `client`; из `files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (хаб эту папку лишь убирает) (файлы в хранилище кладёт хаб по `POST /files`). - `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `api`, `client` и `relay` (реле — клиент своего же HTTP). - Веб-приложение (`web/`) ходит в хаб только через HTTP API (`web/src/api/client.ts`); словари `kind_labels`, `link_types` и набор закрытых статусов `closed_statuses` берёт из `GET /settings` и своих копий не держит. ## Открытые вопросы - Правки из UI в Jira (спринт, срок, связи, исполнитель) — change `viewer-edit`; страницы очереди, агента и настроек — позже. - Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их.