diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 00f1b26..0150036 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -24,8 +24,9 @@ | `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`); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует +`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` отдаёт зеркало без преобразований, diff --git a/CLAUDE.md b/CLAUDE.md index 00ebcc6..e5d691f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ - `uv sync` — установка окружения. - `uv run jira-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов. - `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба. -- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску. +- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`, лента изменений — `/app/feed?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску. - `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур). - `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [--project <метка>]` — MCP по stdio (с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены. - `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх). diff --git a/README.md b/README.md index 173de8a..3b6fdae 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,8 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj | Инструмент | Параметры | Результат | |---|---|---| | `issues` | фильтры как у `GET /issues`, `limit` | таблица задач проекта сессии | -| `issue` | `key` | поля, связи, заметки, последние команды | +| `issue` | `key` | поля, связи, заметки, последние команды, комментарии, 5 последних изменений («изменения (N)») | +| `changes` | `hours`, `since`, `until` (ISO; без смещения — местное время), `kinds` (`status`, `description`, `dates`, `comment`, `other` или `all`), `key` | лента изменений по задачам: что закрылось, правки описания, сроки, новые комментарии; по умолчанию — с начала суток; без привязки — только с `key` | | `notes_set` | `key`, поля заметок | обновлённые заметки | | `batch_create` | `title`, `commands` (у `ensure_attachment` вместо `file` можно `path` — абсолютный путь к локальному файлу до 10 МБ) | `id`, оценка длительности, число команд | | `batch_status` | `id` | статусы команд, ошибки, оценка остатка | @@ -89,7 +90,7 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj | `projects` | нет | проекты хаба: метка, название, доска, тип доски, число задач; проект сессии отмечен | | `project_create` | `label`, `name`, `board_id` (необязательно) | проект заведён, пакет обновления `#N`, готовый фрагмент `.mcp.json` для репозитория проекта | | `agent_status` | нет | онлайн/офлайн, версия, текущая команда, 429 за сутки | -| `sync_log` | `limit` | последние загрузки состояния | +| `sync_log` | `limit` | последние загрузки состояния и «журнал: принят / нет» | | `settings_get`, `settings_set` | ключ, значение | значение / новая `settings_version` | Автор команд из MCP: `claude`; из API без автора: `api`; из веб-приложения: `viewer`. @@ -170,12 +171,14 @@ uv run jira-hub agent publish ../jira-agent/src/agent.py | `GET /agent/commands/{id}` | агент | команда по id | | `POST /agent/commands/{id}/result` | агент | отчёт о выполнении (идемпотентно) | | `POST /agent/state` | агент | загрузка состояния задач пачками | +| `POST /agent/history` | агент | журнал изменений Jira пачками (агент v13+, по `history` в `refresh`) → `{accepted, duplicates}`; повторы отбрасываются, неверная пачка — 422 `bad_history` | | `GET /agent/known-keys` | агент | ключи зеркала | | `GET /agent/release` | агент | опубликованная версия агента `{version, sha256, content}`; 404 `no_release` | | `GET /agent/files/{sha256}` | агент | файл вложения `{file, size, content}` (`content` — base64); 404 `no_file` | | `GET /projects`, `POST /projects` | клиент | проекты `{projects: […]}` (метка, название, `board_id`, `board_type`, число задач) / завести `{label, name, board_id?, author?}` → 201 `{project, batch_id}` и пакет «Новый проект <метка>» (`refresh` по общей JQL); 409 `project_exists`, 422 `bad_project`, 422 `bad_batch` (постановка `refresh` отказана) | | `GET /issues` | клиент | фильтры `project` (метка; неизвестная — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `hide_closed` (`1` — без задач в статусах из настройки `closed_statuses`, по умолчанию «Готово», «Снято»), `limit`, `offset` | -| `GET /issues/{key}` | клиент | задача целиком | +| `GET /issues/{key}` | клиент | задача целиком, с комментариями и журналом изменений `changes` (новые сверху) | +| `GET /changes` | клиент | лента изменений: `since`, `until` (ISO со смещением или `Z`, иначе 422; по умолчанию — последние 24 ч), `project` (только свои задачи), `key`, `kinds`, `limit` (до 2000) → `{since, until, truncated, changes}` | | `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — свои задачи проекта, соседи (связи в обе стороны) и эпики своих задач, с `fact_status`/`work_kind`; связи — те, у которых хотя бы один конец — своя задача проекта (без `project` — все задачи и связи), `last_sync` — последний успешный полный прогон | | `GET /`, `GET /links` | — | перенаправление на `/app/board` | | `PUT /issues/{key}/note` | клиент | заметки (частично) | @@ -184,7 +187,7 @@ uv run jira-hub agent publish ../jira-agent/src/agent.py | `POST /commands/{id}/retry`, `POST /commands/{id}/cancel` | клиент | повтор (новый пакет; `project` в теле — отказ, если команда не из этого проекта) / отмена (`force` для взятой) | | `POST /refresh` | клиент | `{keys}`, `{jql}` или `{full: true}` (общая JQL по меткам всех проектов; проектов нет — 422 «нет проектов») | | `POST /files` | клиент | загрузить файл вложения `{content: base64}` → `{file: sha256, size}`; не base64 — 400, пустой — 422 `empty_file`, больше 10 МБ (10 485 760 байт) — 413 `too_large`; повтор того же содержимого — тот же `file` | -| `GET /agent`, `GET /sync-runs` | клиент | состояние агента (с `release`, `update_state`, `launcher_version`, `agent_reported`, `update_hint`), журнал загрузок | +| `GET /agent`, `GET /sync-runs` | клиент | состояние агента (с `release`, `update_state`, `launcher_version`, `agent_reported`, `update_hint`), журнал загрузок (`history_at` — когда принят журнал изменений полного прогона) | | `GET /settings`, `PUT /settings` | клиент | настройки (PUT частично, поднимает `settings_version`; ответ — все настройки после записи, как у GET); `base_jql` вычисляется из проектов (без проектов — `null`), запись `base_jql` и `board_id` — 422 | ```bash @@ -194,7 +197,7 @@ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8790/issues?sprint= ## Веб-приложение -Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board?project=<метка>`, задачи `/app/issues?project=<метка>`, карточка `/app/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и +Адреса: `http://127.0.0.1:8790/app/` — доска `/app/board?project=<метка>`, задачи `/app/issues?project=<метка>`, лента изменений `/app/feed?project=<метка>` (период и виды событий, история — и в карточке задачи), карточка `/app/issues/{key}` — окно поверх списка или доски (закрытие — ✕, Esc, щелчок по фону; прямой адрес открывает окно поверх списка задач); `/` и `/links` перенаправляют на доску. Вход по клиентскому токену (`uv run jira-hub tokens`), токен хранится в браузере; `localhost` и `127.0.0.1` — разные сайты для браузера, токен и раскладка у каждого свои. Выкатка на Mac mini: diff --git a/docs/superpowers/plans/2026-10-22-web-feed.md b/docs/superpowers/plans/2026-10-22-web-feed.md new file mode 100644 index 0000000..1bb30a2 --- /dev/null +++ b/docs/superpowers/plans/2026-10-22-web-feed.md @@ -0,0 +1,87 @@ +# Лента изменений в веб-приложении — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** страница «Лента» (`/app/feed`) и блок «История» в карточке задачи поверх готовых `GET /changes` и +`GET /issues/{key}.changes`. + +**Architecture:** модуль `web/src/feed/`: чистые функции `events.ts` (период, виды из адреса, текст события, +группировка), компонент `EventList.tsx` (общий для ленты и карточки), страница `FeedPage.tsx`. Маршрут — `shell/pages.tsx`, +раздел — `shell/Sidebar.tsx`, запрос — `api/queries.ts` (`useChanges`, ключ `["changes", …]`), перечитывание после +«Обновить из Jira» — `shell/RefreshProvider.tsx`. + +**Tech Stack:** React 19 + TypeScript, TanStack Query, react-router, Vitest + Testing Library, Playwright. + +**Spec:** `openspec/changes/web-feed/` (proposal, design 1–7, `specs/web-app/spec.md`: MODIFIED «Оболочка и адреса», +ADDED «Лента изменений», «История в карточке задачи»). Контракт хаба — `openspec/specs/hub-api/spec.md` «Лента +изменений», «История задачи в карточке». + +**Worktree:** `/Users/andrey.epifancev/projects/jira-bridge/jira-hub/.wt/feed`, ветка `feat/77-feed` (aep/jira-hub#77). + +## Global Constraints + +- Бэкенд (`src/jira_hub/`) не меняется. +- Тексты событий — ровно по спеке: «статус «было» → «стало»», «описание изменено», «срок ДД.ММ → ДД.ММ» / «плановое начало …» / «плановое окончание …», «комментарий: <300 символов…>», «<подпись>: «было» → «стало»» (значения до 80 символов с «…», пустое — «—»). +- Подписи прочих полей: `assignee` исполнитель, `Sprint` спринт, `labels` метки, `priority` приоритет, `summary` тема, `epic_link` эпик, `Link` связь, `Attachment` вложение, `resolution` резолюция; иначе имя поля как есть. +- Период: `2h` | `today` (по умолчанию, в адрес не пишется) | `yesterday` | `7d`; границы по местному времени, в запрос — `toISOString()`. `limit=2000`. +- Виды: `status`, `description`, `dates`, `comment`, `other`; по умолчанию первые четыре (в адрес не пишутся); подписи флажков «Статусы», «Описание», «Сроки», «Комментарии», «Прочие поля». +- Цвета и отступы — только токены дизайн-системы (`styles/tokens.css`), базовые компоненты из `web/src/ui/`; `dangerouslySetInnerHTML` не появляется; новых зависимостей нет. +- Коммиты — с `(#77)` и строкой `Co-Authored-By: Claude Opus 5.5 `. Зелёный — `make check` и `cd web && npm run e2e`. +- Решения вне спеки и плана — в `openspec/changes/web-feed/design.md`, раздел «Decisions made without discussion», маркер `R-n`. + +## Review Focus + +1. Ответ хаба без `changes` у карточки (старый хаб) и пустой `changes: []` — карточка не падает: нет блока / «изменений нет» (тесты в Task 3). +2. Событие с `at = null`, `author = null`, `from`/`to` = null — строка без «null»/«Invalid Date» (тест в Task 1). +3. Адрес с мусором `period=foo`, `kinds=status,foo` — страница не падает: период по умолчанию, неизвестные виды отброшены (тест в Task 1). +4. Смена проекта в панели на открытой ленте — запрос по новому проекту, период и виды из адреса сохраняются (тест в Task 2). +5. Длинный однословный текст комментария/значения — не ломает ширину (CSS `overflow-wrap: anywhere`) и нет горизонтальной прокрутки на 390 px (e2e в Task 2). + +--- + +### Task 1: `feed/events.ts`, тип события, запрос `useChanges`, перечитывание + +**Files:** +- Create: `web/src/feed/events.ts`, `web/src/feed/events.test.ts` +- Modify: `web/src/api/types.ts` (`ChangeEvent`, `ChangesResponse`, `IssueDetail.changes?: ChangeEvent[]`), `web/src/api/queries.ts` (`qk.changes`, `useChanges`), `web/src/shell/RefreshProvider.tsx` (инвалидация `["changes"]`), `web/src/shell/refresh.test.tsx` (ожидание) + +**Interfaces (produces):** +- `type ChangeKind = "status" | "description" | "dates" | "comment" | "other"`; `interface ChangeEvent { at: string | null; key?: string; summary?: string | null; status?: string | null; kind: ChangeKind; field: string; author: string | null; from: string | null; to: string | null; from_value: string | null; to_value: string | null }`; `interface ChangesResponse { since: string; until: string; truncated: boolean; changes: ChangeEvent[] }`. +- `type Period = "2h" | "today" | "yesterday" | "7d"`; `PERIODS: { value: Period; label: string }[]` (2 часа, Сегодня, Вчера, 7 дней); `parsePeriod(v: string | null): Period`; `periodRange(p: Period, now: Date): { since: Date; until: Date }`. +- `KINDS: { value: ChangeKind; label: string }[]`, `DEFAULT_KINDS`; `parseKinds(v: string | null): ChangeKind[]` (неизвестные отброшены, `null`/пусто → по умолчанию); `kindsParam(k: ChangeKind[]): string | null` (`null` для набора по умолчанию). +- `eventTime(at: string | null, now: Date, alwaysDate = false): string` (`ЧЧ:ММ` / `ДД.ММ ЧЧ:ММ` / «—»); `eventText(e: ChangeEvent): string`; `groupByIssue(events: ChangeEvent[]): { key: string; summary: string | null; status: string | null; events: ChangeEvent[] }[]`. +- `useChanges(q: { project: string; since: string; until: string; kinds: string }, enabled: boolean)` → `useQuery`, ключ `qk.changes(q) = ["changes", q]`, `limit: 2000`. + +- [ ] **Step 1: Tests** — `events.test.ts`: `periodRange` для всех четырёх при `now = new Date(2026, 9, 22, 14, 5)` (today → 22.10 00:00–14:05; yesterday → 21.10 00:00–22.10 00:00; 2h → 12:05; 7d → 15.10 14:05); `parsePeriod("foo") === "today"`; `parseKinds("status,foo") → ["status"]`, `parseKinds(null)` → по умолчанию; `kindsParam(DEFAULT_KINDS) === null`; `eventText` на каждом виде (статус, описание, срок по `from_value` `2026-10-23` → «срок 23.10 → —», плановое окончание со строкой без ISO-значения, комментарий 350 символов → 300 + «…», прочее `assignee` → «исполнитель: «—» → «ivanov»», незнакомое поле — как есть); `eventTime(null, …) === "—"`, сегодня — `ЧЧ:ММ`, вчера — `21.10 ЧЧ:ММ`, `alwaysDate` — с датой; `groupByIssue` — порядок групп по первому появлению. `refresh.test.tsx` — после пакета инвалидируется и `["changes"]`. +- [ ] **Step 2: Run** `cd web && npx vitest run src/feed src/shell/refresh.test.tsx` — FAIL +- [ ] **Step 3: Implement** +- [ ] **Step 4: Run** `make check` — зелёный +- [ ] **Step 5: Commit** — `Лента: события, период и запрос /changes (#77)` + +### Task 2: Страница «Лента», раздел в панели + +**Files:** +- Create: `web/src/feed/FeedPage.tsx`, `web/src/feed/EventList.tsx`, `web/src/feed/feed.css`, `web/src/feed/FeedPage.test.tsx`, `web/e2e/feed.spec.ts` +- Modify: `web/src/shell/pages.tsx` (`/feed` под `ProjectGate`), `web/src/shell/Sidebar.tsx` (`Section` + «Лента», значок `IconHistory` в `ui/icons.tsx`), `web/src/shell/Layout.tsx` (если там список разделов для `section`), `web/src/shell/Sidebar.test.tsx` + +**Interfaces:** +- `FeedPage({ project }: { project: string })` — `PageHeader` «Лента» с подзаголовком «N событий · ДД.ММ ЧЧ:ММ — ДД.ММ ЧЧ:ММ», `Segmented` периода (кнопки, запись в адрес через `setSearchParams`), флажки видов (`Checkbox`), `EventList` по группам; пустые/ошибка/truncated — `Notice` с текстами спеки. +- `EventList({ events, now, alwaysDate }: { events: ChangeEvent[]; now: Date; alwaysDate?: boolean })` — `