Аудит мобильной адаптации и поиска

Работа 19–21 сентября 2026. Проверка шла в двух средах: эмуляция Chromium на 375×812 и реальный мобильный Safari (iOS 18.6, iPhone 16, 393×852) через iOS Simulator. Все цифры ниже — измерения, а не оценки.

Отдельный раздел в конце перечисляет выводы, которые по ходу оказались неверными и были сняты.


1. Агент не отвечал — цепочка из пяти звеньев

Симптом: поле ввода агента вечно висело в «Connecting...», на мобиле и на десктопе одинаково. Причина оказалась не в агенте и не в мобильной вёрстке.

flowchart TD
    A["NATS перезапущен 17.09 08:52"] --> B["chat исчерпал реконнекты<br/>и не переподключился"]
    B --> C["perm.grant не публикуется<br/>последнее сообщение в PERM: 11:17"]
    C --> D["в user_permission нет гранта<br/>последний chat_room: 16.09 14:00"]
    D --> E["auth не кладёт комнату<br/>в claim shared_chat_rooms"]
    E --> F["браузеру запрещена подписка<br/>на live.chat.room-XXXX"]
    F --> G["PERMISSIONS_VIOLATION<br/>вечное Connecting..."]

    style A fill:#3b2f2f,stroke:#a35
    style G fill:#3b2f2f,stroke:#a35

Отказ при этом нигде не был виден. Сервис два дня выглядел живым:

Что подтверждало диагноз: created_by у комнат проставлялся (значит обработчик создания отрабатывал), перевыпуск токена не помогал (и в старом, и в новом ровно 47 комнат), а в списке подключений к :4222 были api, comments, auth, crdt, rag — и не было chat.

Починено перезапуском chat. Три места, где отказ глушится, остались как есть — это отдельная задача.


2. Поиск: ответ «ничего не найдено» обеспечивал сам себя

Самый неприятный баг сессии. В глобальной палитре ветка «No results» проверялась раньше, чем рендерилась группа результатов. Но счётчики задач и кода заполняют сами эти группы через onCount — а они жили внутри else.

flowchart LR
    A["tasksFound = 0"] --> B["ветка No results<br/>выигрывает"]
    B --> C["TaskSearchResults<br/>не монтируется"]
    C --> D["запрос не уходит"]
    D --> A

    style A fill:#2f3b2f,stroke:#5a5
    style D fill:#3b2f2f,stroke:#a35

Компонент, который делает запрос, монтировался только если запрос уже что-то вернул. При включённом одном чипе «Tasks» не уходило ни одного запроса.

Проявлялось не всегда: пока поиск по документации что-то находил, cards.length > 0 ломал цикл и группы монтировались. Поэтому баг был невидим на организациях с проиндексированной документацией и постоянен там, где её нет.

Починено: группы остаются смонтированными и прячутся классом, а строка «No results» показывается поверх них.

было

стало

запросов при одном чипе «Tasks»

[]

/tasks/search

найдено задач

0

5


3. Поиск выбирает движок по контексту страницы

Раньше вкладка «Search» всегда вела в поиск по документации, который требует projectIds. На страницах организации, тасок и репозитория их нет — вкладка принимала ввод и молчала.

flowchart TD
    A["Вкладка Search"] --> B{"Что в контексте?"}
    B -->|"база знаний"| C["Поиск по документации<br/>projects/search"]
    B -->|"организация<br/>таски<br/>карточка задачи"| D["Поиск по задачам<br/>tasks/search"]
    B -->|"репозиторий"| E["Вкладки нет<br/>код ищется на своей странице"]

    C --> F["SearchResultCard"]
    D --> F
    F --> G["контент, диаграммы,<br/>подсветка, автозум"]

    style E fill:#33332a,stroke:#aa5
    style G fill:#2f3b2f,stroke:#5a5

Рендер общий: и задачи, и страницы идут через processResultSearchResultCard. Второго рендера не писалось. Поэтому задача, телом которой является диаграмма, отвечает диаграммой — подтверждено aria-roledescription="flowchart-v2" и переключателем Code | Diagram. Доски tldraw и excalidraw рисуются так же.

Для этого понадобилась правка в rag-v2: индекс задач хранил только плоский content, без original_fragments и content_types. После добавления полей и переиндексации ND-8 отдаёт фрагмент ```mermaid\ngraph TD ... ```, ND-9 — <ExcalidrawBoard>, ND-7 — <TldrawBoard>.

Ещё сделано по поиску: фильтры задач (unresolved, категории статусов, типы), листинг по фильтрам без запроса, сворачиваемые группы со счётчиками, один владелец ⌘K вместо трёх независимых слушателей на window.


4. Мобильная адаптация

Главной проблемой оказался автозум iOS. У полей ввода font-size: 14px при пороге 16px — iOS увеличивает страницу при фокусе и никогда не возвращает масштаб.

flowchart TD
    A["Поле 14px"] --> B["iOS зумит страницу"]
    B --> C["Зум не сбрасывается"]
    C --> D["Кнопка отправки<br/>уезжает за правый край"]
    C --> E["Интерфейс с горизонтальным срезом:<br/>Pages в ages"]
    D --> F["Отправить можно только Enter"]

    style A fill:#3b2f2f,stroke:#a35

Лечится одной строкой — 16px на touch-устройствах. После правки кнопка отправки на месте, вёрстка не съезжает.

Остальное:

что было

что стало

разделы организации недоступны с телефона (hidden md:block без замены)

меню в шапке, доступно и со страницы документа

шапка в 2–4 ряда, свитчер «n...», поиск полоской в 27px

один ряд, скрывается при скролле вниз и отдаёт свою высоту контенту

заголовок Service map зажат в 83px ширины при 335px высоты

переносится, легенда читается

тулбар мермейда в полный экран лежал поверх поля ввода

перенесён в свободный верхний угол

Отдельно подтверждено на реальном устройстве: drag нижнего листа, пинч-зум и поиск в сайдбаре работают корректно.


5. Производительность

Прод-сборка против dev, мобильный вьюпорт:

dev

прод

запросов

250

19

FCP / LCP

684 мс

640 мс

CLS

0.042

0

68% веса экрана логина — декор, а не приложение:

ресурс

передано

background-image.png

509 KB (несжатый PNG)

Inter-Regular.ttf

335 KB (сырой TTF, не WOFF2)

index.js

284 KB gzip / 888 KB распакованного

В репозитории 3.0 MB шрифтов (ни одного WOFF2) и 6.8 MB картинок.

Мерцание измерено. Одна смена организации — 505 мс long tasks, 56 всплесков мутаций DOM, высота хедера меняется 7 раз за 1.1 с, сайдбар трижды размонтируется и монтируется заново. На телефонном CPU это 2–3 с джанка.

Редакторы склеены в один чанк. MdToJson — 746 KB gzip, и в нём mermaid, tldraw, excalidraw и tiptap сразу. Открытие страницы с одной mermaid-диаграммой тянет TLDrawContainer, ExcalidrawContainer и их экспортёры.


6. Что осталось


7. Снятые выводы

Четыре вывода по ходу работы оказались неверными. Оставляю их здесь, потому что каждый показывает, где рассуждение обгоняло проверку.

  1. «Поповер выбора организации прозрачный» — артефакт среды: при скрытой панели браузера framer-motion застывала на середине анимации.

  2. «Enter в агенте не отправляет» — не воспроизвелось в Chromium, но на реальном Safari «Ввод» отправляет.

  3. «Поле поиска перекроется клавиатурой, потому что position: fixed нельзя проскроллить» — это был расчёт, а не наблюдение. Safari поднимает содержимое, поле остаётся видимым.

  4. «Пустая выдача по banank объясняется правами доступа» — объясняло сессию проверяющего (гость в этой организации), а не владельца. Настоящей причиной был дедлок из раздела 2.

Решило дело наблюдение «не уходит ни одного запроса»: именно отсутствие запроса, а не пустой ответ, указало на то, что компонент не монтируется.


Методика

Мобильные проверки стоит гонять на симуляторе, а не только в эмуляции ширины: автозум, перекрытие клавиатурой и выталкивание кнопки отправки в Chromium не воспроизводятся вообще.

Два нюанса окружения: