214 lines
23 KiB
Markdown
214 lines
23 KiB
Markdown
## Context
|
||
|
||
25.10.2026 пять сабагентов сверили открытые follow-up issue хаба и агентов с main (hub 1405ccd). Каждый маркер получил
|
||
статус с доказательством `file:line`. Итоги записаны комментариями в issue. Владелец выбрал первым пакет A — видимые
|
||
пользователю ошибки веба (sdlc-hub#124). Brainstorming-вопросы владелец пропустил («делай сам… не хочу с ними
|
||
сталкиваться в будущем»), поэтому все решения ниже приняты агентом. Они перечислены в «Decisions made without
|
||
discussion».
|
||
|
||
Состав пакета: #42 M-1, M-2, M-4, M-5; #116; #103 С6 и метки `kind-*`; #56 M-4, M-5, M-6; #35 T4.1/M-15; #60 M-3;
|
||
#64 M-6, T9.1; #86 T6.2, T6.3, M-4; #68 T3.3. Ссылки на строки ниже — по main 1405ccd.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
Цель — убрать перечисленные ошибки. Каждую закрепить тестом, который падает до правки: юнит или поведение компонента в
|
||
vitest, для окна карточки дополнительно e2e. Сервер, API, MCP и агенты не меняются.
|
||
|
||
Не цели:
|
||
- прочие маркеры исходных issue: тесты, стиль, доступность UI-кита (#68), квадратичный разбор (#56 M-2), фокус окна
|
||
(#42 M-3), `100vh` окна (#42 M-6);
|
||
- новые возможности.
|
||
|
||
## Decisions
|
||
|
||
### Окно карточки
|
||
|
||
1. **Поиск списка не закрывает окно (#42 M-1).** Сейчас `IssuesPage.tsx:31-38` через 300 мс после ввода делает
|
||
`setParams(..., {replace: true})`. Если за это время открыто окно карточки, запись заменяет адрес окна адресом списка,
|
||
и окно закрывается.
|
||
|
||
Решение — перед открытием карточки со страницы «Задачи» (строка, ключ) несохранённый поиск записывается в адрес сразу,
|
||
таймер снимается. Окно открывается поверх списка уже с этим поиском. Отложенная запись MUST NOT срабатывать, пока
|
||
открыто окно.
|
||
|
||
Реализация на усмотрение исполнителя, например:
|
||
- `IssuesPage` даёт таблице свою обёртку `open`, которая сначала делает flush;
|
||
- или эффект проверяет, что текущий адрес браузера — сам список.
|
||
|
||
2. **Повторное закрытие не уводит назад дальше (#42 M-2).** `overlay.tsx:37-41`: `close()` при каждом вызове делает
|
||
`navigate(-depth)`. Двойной щелчок по ✕ или фону, Esc сразу после щелчка — два шага назад.
|
||
|
||
Решение — закрытие выполняется один раз для одной записи истории окна. Повторные вызовы игнорируются, пока адрес не
|
||
сменился. Признак — `location.key` записи, с которой начато закрытие. Отметка снимается также при «Остаться» (`abortClose`): навигация заблокирована, ключ записи не меняется, и без сброса следующий ✕ молча игнорировался бы (R-15).
|
||
|
||
3. **Подтверждение ухода с несохранённой заметкой (#42 M-4).** `NoteForm.tsx:82`: блок `role="alertdialog"` без имени,
|
||
фокус в него не переводится. Решение:
|
||
- блоку — `aria-label` «Уйти без сохранения заметки?»;
|
||
- при появлении фокус переходит на «Остаться» (безопасное действие по умолчанию);
|
||
- прокрутка к блоку остаётся.
|
||
|
||
4. **Связанная задача «нет в зеркале» и окно 404 (#42 M-5).**
|
||
- В «Связях» (`IssueCard.tsx:123-125`) ключ задачи вне зеркала открывает задачу в Jira в новой вкладке (как «Открыть
|
||
в Jira» в шапке) со значком внешней ссылки, а не окно хаба. Пометка «нет в зеркале» остаётся.
|
||
- Окно 404 (прямой адрес, ключ из текста, гонка) показывает «Закрыть» (закрывает окно целиком, как ✕). Если окно
|
||
открыто переходом из другой карточки (`depth ≥ 1`), добавляется «Назад» — `navigate(-1)` к прежней карточке.
|
||
- То же «Назад» — у ошибки загрузки с «Повторить».
|
||
|
||
### Строка состояния
|
||
|
||
5. **Ответ опроса прежнего проекта отбрасывается (#116).** `ActivityProvider.tsx`:
|
||
- эффект смены проекта (`:75-90`) сбрасывает `activeRef`/`active` и событие;
|
||
- эффект опроса (`:126-154`) в том же коммите видит `active` из замыкания старого рендера и снова ставит событие
|
||
`batch` (`:139`);
|
||
- после этого `active = null`, событие больше никто не снимает.
|
||
|
||
Решение — эффект опроса обрабатывает ответ, только если отслеживаемый пакет на момент обработки тот же:
|
||
`activeRef.current` совпадает с `active` из замыкания (по `id` и `project`). Иначе ответ игнорируется. `finish` по
|
||
устаревшему ответу тоже не вызывается.
|
||
|
||
Тест — детерминированный, без нагрузки: ответ `/batches/{id}` приходит в том же такте, что и смена проекта; после
|
||
этого событие модуля `null`. Существующий тест `activity.test.tsx:331` при этом стабилен (прогон в цикле ≥ 20 раз
|
||
зелёный).
|
||
|
||
### Лента и карточка
|
||
|
||
6. **Текст комментария в ленте без разметки (#103 С6).** Новая чистая функция `wikiPlain(text, opts)` рядом с
|
||
разборщиком (`issues/wiki/`) переводит вики-разметку в обычный текст тем же разбором `parseWiki`:
|
||
- выделение и `{{моно}}` — содержимое;
|
||
- ссылка — её текст (у `[адрес]` — адрес);
|
||
- ключ задачи — ключ, `[~логин]` — «@логин», вложение — имя;
|
||
- блоки — их текст через пробел, заголовок — его текст, `----` — пусто.
|
||
|
||
`eventText` для `comment` берёт `wikiPlain`, затем прежние «одна строка» и обрезку до 300 символов с «…». Модуль ленты
|
||
импортирует функцию из модуля задач того же модуля jira — граница модулей веба не нарушается. Та же функция
|
||
применяется к «Истории» карточки: она рисуется тем же `EventList`.
|
||
|
||
7. **Метки видов работ отдельно (#103).** В «Прочих полях» (`IssueCard.tsx:227`):
|
||
- строка «Вид работы в Jira» — названия видов из `kind_labels`, чьи метки есть у задачи, через запятую, в порядке
|
||
меток задачи;
|
||
- строка «Метки» — остальные метки.
|
||
|
||
Строка с пустым значением, как и сейчас, не показывается. Сравнение меток — точное, как у доски (`dicts.ts`).
|
||
Служебное поле заметки «Вид работы» (`work_kind`) не трогается. Это заметка хаба, а не метка Jira, и они могут
|
||
расходиться — отсюда подпись «в Jira».
|
||
|
||
8. **Описание из пробелов (#56 M-4).** `IssueCard.tsx:79`: раздел «Описание» показывается, только если
|
||
`description.trim()` не пуст.
|
||
|
||
### Вики-разметка (#56 M-5, M-6)
|
||
|
||
9. **`\#`.** `#` добавляется в `ESCAPABLE` (`inline.ts:21`). Строка `\# текст` не список (уже так — `LIST` требует `#`
|
||
первым символом), а `\#` показывается как `#`.
|
||
|
||
10. **Макросы с параметрами.** `MACRO` (`parse.ts:17`) принимает параметры у всех трёх тегов:
|
||
`\{(code|noformat|quote)(?::[^}]*)?\}`. Закрывающий тег — по имени без параметров, как сейчас.
|
||
|
||
11. **Пробелы после открывающего тега.** В `closeMacro` (`parse.ts:111`) остаток строки открывающего тега из одних
|
||
пробелов в тело блока не попадает. Ведущие пробелы строк внутри блока сохраняются.
|
||
|
||
12. **`[~логин|текст]`.** В `bracket` (`inline.ts:149-152`) логин — часть до `|`, обрезанная. Получается «@логин»,
|
||
подпись не показывается. Пустой логин или логин с пробелом — текст как есть, как сейчас.
|
||
|
||
13. **Непарная `[` в ячейке таблицы.** В `row` (`parse.ts:127-132`) `[` увеличивает глубину, только если дальше в
|
||
строке есть парная `]`. Иначе `[` — обычный символ, и `|` делит ячейки.
|
||
|
||
### Список задач, доска, проекты
|
||
|
||
14. **Список ждёт настройки (#60 M-3).** Запрос `GET /issues` (`useIssues`) включается, когда запрос `/settings`
|
||
завершён — успехом или ошибкой. Пока нет — «Загрузка…», без «По этим фильтрам задач нет». При ошибке настроек
|
||
закрытых статусов нет, запрос уходит с `hide_closed=1`, как сейчас.
|
||
|
||
15. **Два быстрых переключения фильтра доски (#35 T4.1/M-15).** `useBoardPrefs.ts:39-54`: `toggle` и `toggleHidden`
|
||
считают новое значение из `prefs` замыкания, и второй клик в том же батче теряет первый. Решение — последнее значение
|
||
держится в `ref`, обновляемом синхронно вместе с `setPrefs`. Новое значение считается из `ref` и тем же значением
|
||
пишется в `localStorage`. Чистота updater-функции сохраняется: StrictMode не пишет дважды.
|
||
|
||
16. **Фильтр спринтов не из-за эпика (#64 T9.1).** `fromHub.ts:30`: `hasSprint` — только по своим задачам, которые
|
||
не эпики (`!isEpic(it) && !it.neighbor && !!it.sprint`), то есть по узлам доски.
|
||
|
||
17. **«Повторить» при ошибке загрузки проектов (#64 M-6).** `ProjectGate.tsx:11`: у сообщения «Проекты не загрузились»
|
||
есть кнопка «Повторить», которая перезапрашивает `GET /projects` (`refetch` запроса проектов). Через контекст
|
||
проекта или прямо — на усмотрение исполнителя.
|
||
|
||
### Вложения
|
||
|
||
18. **Прежняя ошибка не мигает после успешного повтора (#86 T6.2).** После успешного скачивания хук помнит «успех в
|
||
этой карточке» для вложения. Строка (`AttachmentList.tsx:36`) не показывает серверный `state = failed`, пока
|
||
данные карточки не перечитаны. Отметку снимает приход данных, где `state` вложения уже не `failed`, или новый
|
||
`start`.
|
||
|
||
19. **401 при опросе скачивания — экран входа (#86 T6.3).** В `useAttachmentDownload` (`:74`, `:84`) `AuthError` идёт
|
||
в `dropTokenOn(e)`, как в остальном приложении, а не текстом под вложением. `HubDownError` — текст «хаб не отвечает»
|
||
под вложением с «Повторить» (требование «Ошибки хаба»).
|
||
|
||
20. **Порядок вложений — как у хаба (#86 M-4).** Хаб уже отдаёт вложения новыми сверху (`mirror.py:300-301`: по
|
||
`created_at` UTC, затем `attachment_id`). Веб не пересортирует (`newestFirst` убирается из `AttachmentList`).
|
||
`findAttachment` (`inline.ts:36-45`) при одинаковом имени берёт первое вложение в порядке хаба. Так выбор совпадает
|
||
с `attachment_ids` хаба и MCP и не зависит от `Date.parse` строк Jira.
|
||
|
||
### UI-кит
|
||
|
||
21. **Фокус после «Очистить» (#68 T3.3).** `SearchInput` (`ui/inputs.tsx:22`): после нажатия «Очистить» фокус
|
||
переводится в поле ввода (ref). Иначе кнопка исчезает вместе с фокусом.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **#116 — гонка в порядке эффектов.** Решение опирается на то, что эффект смены проекта стоит раньше эффекта опроса и
|
||
синхронно меняет `activeRef`. Тест должен воспроизводить именно один коммит со сменой проекта и ответом опроса, иначе
|
||
он проходит и без правки. Исполнитель проверяет RED.
|
||
- **`wikiPlain` и разбор.** Плоский текст строится разбором карточки. Ошибка разбора в ленте даёт исходник — так же
|
||
безопасно, как в карточке (рендер блока с ошибкой — исходник).
|
||
- **Ссылка «нет в зеркале» в Jira.** Адрес Jira — тот же `JIRA_URL`, что у «Открыть в Jira». Задача может быть и вне
|
||
Jira (опечатка в связи) — тогда Jira покажет свою 404, это приемлемо.
|
||
- **Окно 404 и «Назад».** `depth` берётся из state окна. При прямом адресе `depth = 0` — только «Закрыть».
|
||
|
||
## Decisions made without discussion
|
||
|
||
Все — 25.10.2026: владелец пропустил вопросы brainstorming («делай сам»). Рулинги R-n — с R-1 в пределах фичи.
|
||
|
||
- R-1 (design #1): при открытии карточки отложенный поиск записывается сразу, окно открывается поверх списка с ним.
|
||
Обоснование: пользователь ввёл поиск и видит отфильтрованную строку, на которую щёлкнул, — потерять поиск хуже.
|
||
Цена ошибки — вместо этого отбрасывать поиск: правка одной функции.
|
||
- R-2 (design #2): повторное закрытие окна игнорируется до смены адреса. Цена ошибки — нет.
|
||
- R-3 (design #3): фокус подтверждения ухода — на «Остаться». Обоснование: безопасное действие по умолчанию, данные не
|
||
теряются от лишнего Enter. Цена ошибки — перенести фокус на другую кнопку.
|
||
- R-4 (design #4): ключ связи «нет в зеркале» открывает Jira в новой вкладке, а не окно хаба с 404. Обоснование: в хабе
|
||
про задачу ничего нет, полезное действие — посмотреть её в Jira. Цена ошибки — вернуть окно: одна ветка в `Links`.
|
||
- R-5 (design #4): у окна 404 после перехода по связи — «Назад» (на шаг) и «Закрыть» (всё окно). Цена ошибки — убрать
|
||
кнопку.
|
||
- R-6 (design #5): #116 чинится в провайдере (ответ сверяется с текущим `activeRef`), тест не ослабляется. Цена ошибки
|
||
— нет: гипотеза подтверждена чтением кода при сверке.
|
||
- R-7 (design #6): лента показывает текст комментария без разметки по разбору карточки; «История» карточки — тоже.
|
||
Альтернатива — показывать начало комментария отрендеренным — отвергнута: строка ленты однострочная. Цена ошибки —
|
||
правка `eventText`.
|
||
- R-8 (design #7): метки видов работ — отдельной строкой «Вид работы в Jira» с названиями видов, из «Меток» убраны.
|
||
Альтернатива — просто скрыть. Отвергнута: метка вида — единственный след вида работы в Jira, и он может расходиться
|
||
с заметкой. Цена ошибки — скрыть строку.
|
||
- R-9 (design #12): `[~логин|текст]` показывается «@логин» без подписи. Jira такой формы не документирует, логин важнее
|
||
подписи. Цена ошибки — показывать подпись.
|
||
- R-10 (design #13): непарная `[` в строке таблицы — обычный символ. Цена ошибки — нет.
|
||
- R-11 (design #14): при ошибке `/settings` список всё равно запрашивается (с `hide_closed=1`, закрытых статусов нет),
|
||
а не блокируется. Обоснование: список полезнее пустого экрана, ошибку настроек покажет страница. Цена ошибки — нет.
|
||
- R-12 (design #19): `HubDownError` при опросе скачивания остаётся текстом под вложением с «Повторить», отдельного
|
||
экрана нет — данные карточки остаются на экране (требование «Ошибки хаба»). Цена ошибки — нет.
|
||
- R-13 (design #20): веб берёт порядок вложений у хаба, а при одинаковых именах — первое в этом порядке. Обоснование:
|
||
совпадение с `attachment_ids` хаба и MCP. Цена ошибки — вернуть сортировку на клиенте.
|
||
- R-14 (proposal): в пакет не взяты #42 M-3 (возврат фокуса после закрытия окна), M-6 (`100vh`), доступность
|
||
UI-кита #68 и производительность разбора #56 M-2. Это не ошибки поведения, они остаются в своих issue. Цена ошибки —
|
||
следующий пакет.
|
||
- R-15 (design #2, 26.10.2026): `closingKey` в `overlay.tsx` сбрасывается и в `abortClose` («Остаться»), а не только при смене
|
||
`location.key`. Обоснование: при заблокированной навигации ключ записи не меняется, и без сброса второй Esc/✕ после
|
||
«Остаться» молча игнорировался бы (дефект плана, пойман e2e Task 4). Цена ошибки — повторный ✕ при блокере станет
|
||
повторно вызывать navigate; безвредно.
|
||
- R-16 (design #6): в ленте вложение читается именем без списка вложений карточки — флаг `WikiOptions.anyAttachment`,
|
||
выставляется только `wikiPlain`; карточка его не передаёт. Обоснование: финальное ревью I-1, `!скрин.png|thumbnail!` и
|
||
`[^отчёт.pdf]` иначе остаются сырой разметкой. Цена ошибки — правка в одном месте.
|
||
- R-17: #18 выполнен частично (отметка «успех» не снимается приходом данных) и разбор всего комментария ради 300 символов
|
||
в ленте — отложены (M-2, M-6 финального ревью). Обоснование: редкий случай и общая тема #56 M-2. Цена ошибки — редкая
|
||
невидимая ошибка вложения в открытой карточке; задержка ленты на очень длинных комментариях.
|
||
- R-18 (design #2): ссылка R-15 указывает на решение 2, а не на несуществующее #22; к решению 2 дописан сброс в
|
||
`abortClose`. Цена ошибки — нет (документация).
|
||
- R-19: N-1 (пере-ревью финальной волны) — в режиме `anyAttachment` ветка `!имя!` принимает любую пару «!», поэтому в ленте
|
||
«Срочно! Проверьте! Ок» теряет восклицательные знаки — отложено в follow-up (требовать расширение файла в имени);
|
||
второй волны правок нет. Цена ошибки — искажённые «!» в тексте строки ленты, данные не страдают.
|