24 KiB
Raw Blame History

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) к прежней карточке.
    • То же «Назад» — у ошибки загрузки с «Повторить».

Строка состояния

  1. Ответ опроса прежнего проекта отбрасывается (#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 раз зелёный).

Лента и карточка

  1. Текст комментария в ленте без разметки (#103 С6). Новая чистая функция wikiPlain(text, opts) рядом с разборщиком (issues/wiki/) переводит вики-разметку в обычный текст тем же разбором parseWiki:

    • выделение и {{моно}} — содержимое;
    • ссылка — её текст (у [адрес] — адрес);
    • ключ задачи — ключ, [~логин] — «@логин», вложение — имя;
    • блоки — их текст через пробел, заголовок — его текст, ---- — пусто.

    eventText для comment берёт wikiPlain, затем прежние «одна строка» и обрезку до 300 символов с «…». Модуль ленты импортирует функцию из модуля задач того же модуля jira — граница модулей веба не нарушается. Та же функция применяется к «Истории» карточки: она рисуется тем же EventList.

  2. Метки видов работ отдельно (#103). В «Прочих полях» (IssueCard.tsx:227):

    • строка «Вид работы в Jira» — названия видов из kind_labels, чьи метки есть у задачи, через запятую, в порядке меток задачи;
    • строка «Метки» — остальные метки.

    Строка с пустым значением, как и сейчас, не показывается. Сравнение меток — точное, как у доски (dicts.ts). Служебное поле заметки «Вид работы» (work_kind) не трогается. Это заметка хаба, а не метка Jira, и они могут расходиться — отсюда подпись «в Jira».

  3. Описание из пробелов (#56 M-4). IssueCard.tsx:79: раздел «Описание» показывается, только если description.trim() не пуст.

Вики-разметка (#56 M-5, M-6)

  1. \#. # добавляется в ESCAPABLE (inline.ts:21). Строка \# текст не список (уже так — LIST требует # первым символом), а \# показывается как #.

  2. Макросы с параметрами. MACRO (parse.ts:17) принимает параметры у всех трёх тегов: \{(code|noformat|quote)(?::[^}]*)?\}. Закрывающий тег — по имени без параметров, как сейчас.

  3. Пробелы после открывающего тега. В closeMacro (parse.ts:111) остаток строки открывающего тега из одних пробелов в тело блока не попадает. Ведущие пробелы строк внутри блока сохраняются.

  4. [~логин|текст]. В bracket (inline.ts:149-152) логин — часть до |, обрезанная. Получается «@логин», подпись не показывается. Пустой логин или логин с пробелом — текст как есть, как сейчас.

  5. Непарная [ в ячейке таблицы. В row (parse.ts:127-132) [ увеличивает глубину, только если дальше в строке есть парная ]. Иначе [ — обычный символ, и | делит ячейки.

Список задач, доска, проекты

  1. Список ждёт настройки (#60 M-3). Запрос GET /issues (useIssues) включается, когда запрос /settings завершён — успехом или ошибкой. Пока нет — «Загрузка…», без «По этим фильтрам задач нет». При ошибке настроек закрытых статусов нет, запрос уходит с hide_closed=1, как сейчас.

  2. Два быстрых переключения фильтра доски (#35 T4.1/M-15). useBoardPrefs.ts:39-54: toggle и toggleHidden считают новое значение из prefs замыкания, и второй клик в том же батче теряет первый. Решение — последнее значение держится в ref, обновляемом синхронно вместе с setPrefs. Новое значение считается из ref и тем же значением пишется в localStorage. Чистота updater-функции сохраняется: StrictMode не пишет дважды.

  3. Фильтр спринтов не из-за эпика (#64 T9.1). fromHub.ts:30: hasSprint — только по своим задачам, которые не эпики (!isEpic(it) && !it.neighbor && !!it.sprint), то есть по узлам доски.

  4. «Повторить» при ошибке загрузки проектов (#64 M-6). ProjectGate.tsx:11: у сообщения «Проекты не загрузились» есть кнопка «Повторить», которая перезапрашивает GET /projects (refetch запроса проектов). Через контекст проекта или прямо — на усмотрение исполнителя.

Вложения

  1. Прежняя ошибка не мигает после успешного повтора (#86 T6.2). После успешного скачивания хук помнит «успех в этой карточке» для вложения. Строка (AttachmentList.tsx:36) не показывает серверный state = failed, пока данные карточки не перечитаны. Отметку снимает приход данных, где state вложения уже не failed, или новый start.

  2. 401 при опросе скачивания — экран входа (#86 T6.3). В useAttachmentDownload (:74, :84) AuthError идёт в dropTokenOn(e), как в остальном приложении, а не текстом под вложением. HubDownError — текст «хаб не отвечает» под вложением с «Повторить» (требование «Ошибки хаба»).

  3. Порядок вложений — как у хаба (#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-кит

  1. Фокус после «Очистить» (#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 (требовать расширение файла в имени); второй волны правок нет. Цена ошибки — искажённые «!» в тексте строки ленты, данные не страдают.
  • R-20 (design #6, 26.10.2026, решение владельца по R-19): N-1 исправлена до PR, а не отложена. В режиме anyAttachment !имя! без списка вложений считается вложением, только если имя — имя файла с расширением без пробелов по краям (FILE_NAME в inline.ts); [^имя] — явная ссылка на вложение, проверка к ней не применяется. Обоснование: ветка сама внесла искажение «!» в строке ленты, владелец не хочет таких ошибок. Цена ошибки — !слово.слово! в обычном тексте ленты («Ура!Сделано.ру!») прочитается как имя вложения без «!»; данные не страдают.