Панель отладки
Выделить кусок экрана прямо в работающем приложении, нарисовать поверх, описать словами, отправить. Тикет ложится в базу вместе с кадром и всем техническим контекстом момента, а позже его забирает командная строка — и чинит нейронка.
Баг, замеченный с телефона внутри Telegram, раньше можно было только запомнить и потом пересказать. Пересказ теряет ровно то, что нужно для починки: какой был экран, какая сборка, что лежало в консоли, как это выглядело.
Как открыть
Правило одно: жест или сочетание клавиш показывают значок, значок открывает панель. Показанный однажды, значок остаётся до конца вкладки — иначе после каждого отчёта пришлось бы вспоминать жест заново.
| Где | Как |
|---|---|
| Клавиатура, где угодно | Ctrl + Shift + Q |
| Телефон | три пальца, удержание, в любом месте экрана |
| Клиент Telegram на компьютере | t.me/<бот>/app?startapp=devkit — значок виден сразу |
Своя машина, localhost или домашняя сеть | значок виден сразу, без жеста |
| Телефон на dev-сервере через туннель | дописать ?devkit=1 к адресу |
| Приглашённый тестировщик | ссылка с ключом — значок виден сразу, см. ниже |
| Выключить принудительно | ?devkit=0 — этим же параметром закрывается панель на съёмке кадров документации |
Именно Ctrl, а не Cmd, и на макбуке тоже: Cmd + Shift + Q там выходит из системы, и перехватить это нельзя — такие сочетания браузеру не отдают вовсе.
В клиенте Telegram на компьютере сочетание может не сработать, и это не чинится со стороны страницы: клиент — обычное настольное приложение со своими горячими клавишами, и то, что он забрал себе, до мини-аппа не доходит. Поэтому там есть ссылка ?startapp=devkit: её клиент открывает всегда, значок виден сразу, и запоминается он на всю сессию.
Жест на телефоне — это буквально: положить на экран три пальца сразу и подержать около секунды, в любом месте. Отпустите — появится значок. Три одновременных касания выбраны не наугад: долгое нажатие в приложении — 480 мс одним пальцем, перетаскивание строк — тоже один палец, вертикальный свайп Telegram выключен, зум запрещён. Случайно так не выйдет ни у кого.
Запоминать жест обычно не нужно: и себе, и тестировщику проще открыть приложение по ссылке с ключом — там значок виден сразу. Жест остаётся запасным ходом для сборки, открытой без всяких параметров.
Три одновременных касания выбраны не случайно: долгое нажатие — 480 мс одним пальцем, перетаскивание строк — один указатель с захватом, вертикальный свайп Telegram выключен, зум запрещён. Панель только наблюдает за касаниями и никогда не вмешивается в них.
Внутри демо панель не появляется: приложение показывают другому человеку, и его кадр экрана нам не нужен. На своей машине демо, наоборот, панель не выключает — это обычный режим разработки.
Не только в приложении
Панель стоит и на этом сайте, и на лендинге: опечатка в тексте страницы или разъехавшаяся вёрстка ловятся тем же кадром с разметкой, что и баг в приложении. Нажмите Ctrl + Shift + Q прямо здесь.
Сюда она попадает не импортом, а готовыми файлами:
bash
npm run devkit:sync # собрать и разложить по docs/public/ и landing/public/Иначе никак: документация и лендинг — отдельные проекты Vercel с Root Directory = docs и = landing, и каталог src/ приложения в их сборку не попадает. Тот же приём, что и с иконками (npm run brand). Расхождение копий ловит tools/devkit.test.ts при обычном npm test.
Вес на странице документации — около трёх килобайт сжатыми: это проверка доступа, сочетание клавиш и сам значок. React, съёмка кадра и слой приезжают отдельными кусками только при первом открытии панели, и сторож в тестах не даёт входному файлу растолстеть.
Входа на этих сайтах нет и быть не может, поэтому тикет оттуда уходит только с ключом приглашения — ?test=<ключ> в адресе, один раз на вкладку. У такого тикета нет отправителя, и в уведомлении он подписан «по ссылке»; предел в двадцать открытых тикетов у всех безымянных общий.
Позвать помочь с тестированием
Жест тремя пальцами хорош ровно до того момента, когда помогать зовут другого человека: объяснить его в переписке дороже, чем получить отчёт. Поэтому у тестировщика своя дверь — ссылка с ключом.
Ключ ставится один раз:
bash
node -e "console.log(require('crypto').randomBytes(12).toString('base64url'))"
npx --prefix worker wrangler secret put DEVKIT_INVITEДальше человеку отправляется одна из двух ссылок:
https://app.mypriorities.life/?test=<ключ>
t.me/<бот>/app?startapp=test_<ключ>Он открывает её, видит кнопку «Отладка» и дальше делает всё то же самое, что и вы. Ни номер Telegram спрашивать, ни в белый список вписывать не нужно.
Что при этом остаётся под контролем:
- тикет всегда подписан. Ключ не заменяет вход, а дополняет его: отправитель берётся из его аккаунта Telegram и попадает в строку тикета и в уведомление боту. Анонимных тикетов не существует;
- ключ отзывается одной командой — новый
wrangler secret put DEVKIT_INVITEделает все старые ссылки бесполезными. Поэтому он и не сохраняется на устройстве: носитель ключа — только ссылка; - потолок в двадцать открытых тикетов на человека остаётся общим для всех;
- секрет не задан — приглашений не существует. Не «пускаем всех», а «такой двери нет».
Ключ слабее белого списка, и это осознанно: он живёт в ссылке, а ссылки пересылают. Открывает он ровно одно — право завести тикет.
Что уходит и что не уходит
Приложение хранит настоящие приоритеты, навыки и историю энергии. Поэтому правила приватности здесь механические, а не «мы стараемся».
Уходит:
- кадр выделенной области — ровно тот, который показан перед отправкой;
- отметка сборки, экран, время со смещением, размеры окна и плотность;
- платформа и версия клиента Telegram, признаки «демо», «гость», «установлено»;
- последние 30 записей журнала ошибок и предупреждений;
- путь до элемента, которого коснулись, и его разметка без текста;
- снимок состояния — только числа, булевы значения и перечисления:
{ priorities: 7, modules: ['skills'], clickDays: 214, sync: 'signed-in' }.
Не уходит никогда:
- названия приоритетов, имена навыков, любые написанные вами строки. Ключи с такими именами выбрасываются машинно, строки длиннее сорока символов превращаются в многоточие, почта, телефон, адрес с параметрами и JWT узнаются по форме (
src/devkit/redact.ts); initDataи токены. Панель не импортируетtelegram/sdk.tsвовсе, поэтому структурно не может их прочитать; токен доступа спрашивается у приложения в момент отправки и не пишется ни в тикет, ни в очередь;- содержимое элементов в разметке: теги и классы остаются, текст становится многоточием.
Единственное место, где личное всё-таки может уйти, — сам кадр. Поэтому выделение области и есть инструмент вычистки: по умолчанию снимается не весь экран, «Всё окно» вынесено отдельной подписанной кнопкой, а готовый вырез показывается в полный размер до нажатия «Отправить».
Тикеты и кадры хранятся месяц: кадры гасит сам KV по сроку хранения, строки убирает ночная задача.
Как это устроено
Порядок обратный привычному: сначала снимается весь экран, и только потом выделяется область. Наивный порядок — затемнить, потянуть рамку, снять — в живом приложении ломается тремя способами: тост успевает исчезнуть посреди перетаскивания, затемнение рискует попасть в растр, а координаты относятся к странице, которая за это время изменилась.
Панель живёт в отдельном корне React, рядом с приложением, а не внутри него. Непойманное исключение размонтирует весь корень, в котором случилось; белый экран — самый ценный тикет на свете, и панель внутри того же дерева гарантировала бы, что завести его нельзя.
Отказ съёмки не блокирует отчёт: тикет уходит текстом, а причина едет полем shotError.
Очередь черновиков живёт в своей базе IndexedDB (mypri-devkit) и досылается, когда вернулась сеть, вернулись на вкладку или прошло несколько секунд после запуска. Признак «есть что досылать» лежит в localStorage одной строкой — чтобы запуск, которому досылать нечего, не грузил лишний код вовсе.
Настройка
Приложение — .env.local (см. .env.example):
VITE_DEVKIT_URL=https://api.mypriorities.lifeНе задан — панели нет вовсе, и это нормальная сборка. Отдельно от VITE_SYNC_URL намеренно: адрес синхронизации гасится в демо, и панель молча исчезала бы ровно там, где чаще всего смотрят на интерфейс. Не задан — приложение само откатывается на VITE_SYNC_URL.
Сервер — worker/wrangler.toml и секреты:
bash
wrangler kv namespace create SHOTS # id подставить в wrangler.toml
wrangler secret put DEVKIT_TOKEN # ключ для командной строки
npm --prefix worker run db:remote # применить миграцииDEVKIT_ALLOW в [vars] — номера Telegram через запятую, кому можно заводить тикеты. Пусто — нельзя никому: закрыто по умолчанию, потому что цена ошибки в эту сторону — чужие кадры экрана в нашей базе. Свой номер печатает npm --prefix worker run chat-id. Тестировщиков сюда вписывать не нужно — им ссылка с ключом.
Разработка на своей машине. Вход через Telegram на localhost невозможен — бот такого домена не знает. Поэтому в .env.local можно положить
VITE_DEVKIT_DEV_TOKEN=<то же значение, что и DEVKIT_TOKEN>Эта ветка читается только под import.meta.env.DEV: в боевой сборке она вырезается целиком вместе со ссылкой на переменную, и токен структурно не может оказаться в бандле.
Разбор: страница тикетов
https://api.mypriorities.life/devkit/adminВход — тот же ключ DEVKIT_TOKEN, которым работает командная строка. Он живёт в сессии вкладки и уходит заголовком; в адресе его нет никогда, иначе он оседал бы в истории браузера и в журналах. Разметка отдаётся и без ключа — сама по себе она пустая, а данные без ключа не отдаются ни на одном маршруте.
Четыре списка по состояниям и одно правило перехода между ними:
| Состояние | Что значит |
|---|---|
| Новые | пришло, ещё не смотрели |
| В работе | посмотрели и отправили чинить — это и есть очередь для tickets:pull |
| Починено | закрыто из командной строки после правки |
| Не чиним | разобрались и решили не чинить |
Открыв тикет, видно кадр (нажатие разворачивает его целиком), путь до элемента, журнал ошибок и снимок состояния. Описание можно поправить прямо здесь — и это не украшение: человек, поймавший баг, пишет второпях и своими словами, а чинить по такому тексту значит гадать. Одна переписанная строка экономит потом полчаса.
Внизу, отдельной строкой и другим цветом, — «Удалить»: тикет уходит вместе с кадром и насовсем. Соседство с «Сохранить» рано или поздно кончилось бы промахом, а отменить это нечем, поэтому кнопка спрашивает подтверждение и называет номер.
Кнопка «Отправить на фикс» переводит тикет в работу. Дальше он попадает в npm run tickets:pull — и только он: входящий поток это сырые жалобы, среди которых бывают и повторы, и «показалось», а отбор глазами не должен делаться дважды.
Как забрать тикеты
bash
npm run tickets:pull # забрать очередь на починку
npm run tickets:pull -- --open # забрать новые, минуя отбор
npm run tickets:list # что пришло, одной строкой на тикет
npm run tickets:close -- a3f9c1 "что сделано" # закрыть и убрать копию
npm run tickets:close -- a3f9c1 "не воспроизводится" --wontfixКаждый тикет — три файла: ticket.md (написан для чтения сверху вниз, самое полезное вверху), кадр и payload.json. Каталог .tickets/ закрыт .gitignore: внутри кадры экрана с настоящими данными.
В Claude Code весь разбор делает навык /tickets — забрать, прочитать вместе с картинкой, найти код по экрану и имени класса, воспроизвести, починить, закрыть.
Оговорки
backdrop-filterв кадре не рисуется. Растеризация идёт черезforeignObject, а размытие подложки там не поддерживается: таб-бар выйдет плоским, а не размытым. Это косметика кадра, а не баг приложения.- Кадр — это видимое окно, а не вся страница. На многостраничном сайте снимается ровно то, что на экране; закреплённые шапка и меню возвращаются на свои места вручную (внутри
foreignObject«прилипание» не существует). - Встроенные фреймы в кадр не попадают. Содержимое чужого домена браузер не отдаёт никому — на лендинге на месте рамки телефона остаётся заставка под ней.
- Картинки, которые ещё не загрузились, не ждут. Ленивые изображения ниже сгиба браузер не начнёт грузить, пока до них не долистают, и ожидание упёрлось бы в срок целиком. Полторы секунды на догрузку, дальше кадр снимается как есть: то, чего нет на экране, в кадр всё равно не попало бы.
- KV согласован в конечном счёте. Кадр, запрошенный сразу после отправки, может секунду отдавать 404. Для командной строки, которая забирает тикеты позже, это неважно.
- Если появится CSP. Сейчас политики нет. Когда она появится, растеризации понадобятся
img-src data: blob:иfont-src data:— иначе съёмка сломается молча. - Отправка требует входа. Сервер узнаёт отправителя по обычному токену сессии: общий ключ в бандле ключом не является.
Перенести в другой проект
Каталог src/devkit/ не импортирует из приложения ничего — это проверяет tools/deps.test.ts. Перенос:
- скопировать каталог;
npm i modern-screenshot;- один вызов в точке входа:
mountDevkit({ endpoint, app, build }). Остальные поля адаптера (route,snapshot,haptics,backButton, …) необязательны и добавляются потом; - сервер: либо направить
endpointв уже развёрнутый Worker с другим значениемapp— колонкаappсуществует ровно для этого, — либо скопироватьworker/src/devkit.tsс миграцией и вписать блок маршрутов; - скопировать
tools/tickets/, три npm-скрипта, строку в.gitignoreи навык.
Рядом
Архитектура · Демо-режим · Данные и синхронизация · Сборка и публикация