Диагностика + починка данных прод-Rails: kamal exec, полноширинный `?`, Base64, непрерывность треда.
Продакшн-400/500 совсем не то же, что локальные. Локально ты перезапускаешь тесты, правишь, снова запускаешь. В проде пользователь уже нажал «Оплатить» и смотрит на белый экран.
Три пути:
rails console, изменения без аудита, одна опечатка — и ты в бедеЭта статья про третий путь. Два реальных случая: полноширинный ?, залезший в credentials и положивший x402-платежи 500-ми, и твит в середине треда со status: :failed, который нужно повторить, сохранив целостность треда. По пути Claude будет пытаться свернуть в 4 неверные стороны по умолчанию — каждую надо перехватить.
kamal app exec --reuseKamal — инструмент деплоя Rails от 37signals. У него есть команда:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse означает: не поднимать новый контейнер, а выполнить внутри уже работающего web-контейнера. Никакого нового билда, никакого docker pull, никакого рестарта, никакой повторной инжекции ENV. Команда выполняется, контейнер возвращается к обработке запросов.
Вывод течёт через stdout к тебе в терминал — фактически puts в продакшн-Rails-консоли, без SSH, tmux или отрыва от ноутбука.
Типичная сессия:
$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847
Туда-обратно 3-6 секунд. На два порядка быстрее передеплоя.
?, вызвавший прод-500Commit eba9ac9.
Симптом: Через несколько часов после запуска x402-платежей каждый платёжный запрос валился в 500. Логи полны Net::HTTPBadResponse от HTTP-клиента. Локально — идеально.
Диагностика: Сначала попроси Claude напечатать x402-конфиг в проде:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Вывод:
"0xAbC123...def?"
На хвосте лишний ? — полноширинный вопросительный знак (U+FF1F), а не полуширинный ?. У кого-то во время правки config/credentials/production.yml.enc переключился IME, и этот символ влез.
Локальный config/credentials.yml.enc (расшифрованный dev/test master.key) этого не содержит — в Rails 8 production и dev — разные encrypted credentials, содержимое не общее.
Фикс: Нельзя SSH и править файл напрямую (зашифрован), нельзя стянуть и править локально (master.key не на ноуте). Правильный ход — дать Claude написать одноразовый Ruby-скрипт и заинжектить через EDITOR= в credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Убрать полноширинный вопросительный знак с хвоста
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
bin/rails credentials:edit --environment production
Поток credentials:edit: расшифровать → записать во временный файл → вызвать $EDITOR → перезашифровать → удалить временный. Заменить EDITOR на наш Ruby-скрипт — и редактирование автоматизировано, не надо пялиться на шифртекст локально.
Потом git commit + kamal deploy один раз — этот деплой обязателен, потому что production.yml.enc изменился. Но диагностика деплоя не стоила.
Правило: Когда прод ломается, сначала читай через kamal app exec --reuse. Не гадай, не передеплой сначала.
Симптом: После публикации статьи твиты уходят в таблицу x_queue_tweets. Один из них — 2-й из 4-твитового треда — застревает со status: :failed (рейт-лимит X API, валидация контента, что угодно). Повтор требует сохранить целостность треда с 1-м твитом.
Найти упавший:
kamal app exec --reuse 'bin/rails runner "
XQueue::Tweet.where(status: :failed).each do |t|
puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
end
"'
Оказывается id=87, thread_id=15, thread_position=2.
Ловушка 1: shell-экранирование. В содержимом твитов часто кавычки, переводы строк, backtick. Если написать:
# взорвётся — shell съест кавычки и обратные слеши
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Танец Base64 — кодируй локально, передавай base64-строку, декодируй внутри runner:
# Кодируем локально
echo -n 'Переписанное содержимое твита...' | base64
# => 0J/QtdGA0LXQv9C40YHQsNC90L3QvtC1INGB0L7QtNC10YDQttC40LzQvtC1INGC0LLQuNGC0LAuLi4=
# Передаём
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('0J/QtdGA0LXQv9C40YHQsNC90L3QvtC1INGB0L7QtNC10YDQttC40LzQvtC1INGC0LLQuNGC0LAuLi4='), status: :scheduled)
puts t.status
\""
Base64-строки — чистый ASCII, для shell безопасны.
Ловушка 2: целостность треда. XQueue::PostTweetJob.perform_later(87) публикует твит сам по себе — не цепляется к твиту #1 — потому что X API нужен reply_to_tweet_id, а Job по умолчанию такого значения не несёт.
Найти x_tweet_id предыдущего твита (успешные отправки заполняют это поле):
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789
Поставить в очередь с reply-целью:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
polling_interval у воркера — 0,1 секунды — он подхватывает job почти сразу. Через пару секунд kamal app exec смотрит, что status перешёл из scheduled в posted, а x_tweet_id заполнился — тред непрерывен.
Правило: Операции с данными в проде должны уважать ограничения бизнес-уровня, а не только «запись обновлена успешно». Целостность треда — бизнес-ограничение; Rails runner её за тебя не проверяет.
При такой работе первый рефлекс Claude часто ошибочен. Ловим каждый:
«Я зайду по SSH и посмотрю...»
Перенаправь: kamal app exec --reuse лучше SSH — внутри контейнера, окружение Rails загружено, аудируется (kamal-логи сохраняют запись), не трогает shell хоста, нет контейнерного дрейфа (reuse гарантирует текущую прод-версию).
«Напишу migration, чтобы выпилить полноширинный
?из wallet_address...»
Перенаправь: Изменение одного значения credentials не требует migration (БД не трогали). Одноразовый Rails runner делает это за 10 секунд; migration требует деплоя и остаётся в схеме навсегда. Migration — только если фикс может потребоваться снова; опечатка — разовая штука.
«Просто вызову
update!(content: "...")...»
Перенаправь: Любой пользовательский контент (твиты, комментарии, введённый markdown) должен идти через Base64. Парсинг shell кавычек, обратных слешей, $ и backtick — классическое минное поле — прод не место для тренировок.
perform_later без бизнес-параметров«Просто перезапущу
PostTweetJob.perform_later(87)...»
Перенаправь: Сначала спроси: «связана ли эта запись с другими?» У тредов — reply_to, у батч-джобов — batch_id, у пагинированных — cursor. Список аргументов джоба — носитель этих бизнес-связей. Выкинешь аргумент — разорвёшь цепь.
Отладка + модификация данных прод-Rails с Claude — 6 правил:
kamal app exec --reuse 'bin/rails runner "puts X"'. Локализуй проблему, прежде чем что-либо менять.kamal app exec --reuse — инструмент по умолчанию, не SSH, не передеплой. Внутри контейнера, Rails загружен, туда-обратно 3-6 секунд.EDITOR=ruby-script bin/rails credentials:edit --environment production. Ruby-скрипт делает правку, не пялимся на шифртекст локально.echo -n 'X' | base64 локально, Base64.decode64 в runner.Отладка в проде — не пересадка dev-флоу в прод — у тебя нет пространства для итерации и нет допуска ошибок. На самом деле ты используешь поверхность интроспекции, которую прод уже предоставляет (Rails runner + kamal exec + credentials:edit), каждый шаг — минимально возможное изменение. Claude умеет писать корректный Ruby — но знание «что можно делать напрямую, а что надо сперва диагностировать» — решение, которое он за тебя не примет. Это твоя продовая дисциплина.